diff --git a/csl.api/nbproject/project.properties b/csl.api/nbproject/project.properties --- a/csl.api/nbproject/project.properties +++ b/csl.api/nbproject/project.properties @@ -40,7 +40,7 @@ # Version 2 license, then the option applies only if the new code is # made subject to such option by the copyright holder. -spec.version.base=2.34.0 +spec.version.base=2.35.0 is.autoload=true javac.source=1.6 diff --git a/csl.api/nbproject/project.xml b/csl.api/nbproject/project.xml --- a/csl.api/nbproject/project.xml +++ b/csl.api/nbproject/project.xml @@ -119,7 +119,7 @@ 1 - 1.6 + 1.34 diff --git a/csl.api/src/org/netbeans/modules/csl/api/StructureScanner.java b/csl.api/src/org/netbeans/modules/csl/api/StructureScanner.java --- a/csl.api/src/org/netbeans/modules/csl/api/StructureScanner.java +++ b/csl.api/src/org/netbeans/modules/csl/api/StructureScanner.java @@ -50,6 +50,8 @@ import org.netbeans.api.annotations.common.CheckForNull; import org.netbeans.api.annotations.common.NonNull; import org.netbeans.modules.csl.spi.ParserResult; +import org.netbeans.api.editor.fold.FoldType; +import org.netbeans.spi.editor.fold.FoldTypeProvider; /** * Given a parse tree, scan its structure and produce a flat list of @@ -69,8 +71,20 @@ @NonNull List scan(@NonNull ParserResult info); /** - * @todo Do this in the same pass as the structure scan? * Compute a list of foldable regions, named "codeblocks", "comments", "imports", "initial-comment", ... + * The returned Map must be keyed by {@link FoldType#code}. For backward compatibility, the following + * tokens are temporarily supported although no FoldType is registered explicitly. + * + * This additional support will cease to exist after NB-8.0. Language owners are required to register + * their {@link FoldTypeProvider} and define their own folding. */ @NonNull Map> folds(@NonNull ParserResult info); diff --git a/csl.api/src/org/netbeans/modules/csl/editor/fold/GsfFoldManager.java b/csl.api/src/org/netbeans/modules/csl/editor/fold/GsfFoldManager.java --- a/csl.api/src/org/netbeans/modules/csl/editor/fold/GsfFoldManager.java +++ b/csl.api/src/org/netbeans/modules/csl/editor/fold/GsfFoldManager.java @@ -51,9 +51,11 @@ import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.TreeMap; import java.util.TreeSet; import java.util.WeakHashMap; @@ -65,9 +67,11 @@ import javax.swing.event.DocumentEvent; import javax.swing.text.BadLocationException; import javax.swing.text.Document; -import javax.swing.text.Position; import org.netbeans.api.editor.fold.Fold; +import org.netbeans.spi.editor.fold.FoldInfo; +import org.netbeans.api.editor.fold.FoldTemplate; import org.netbeans.api.editor.fold.FoldType; +import org.netbeans.api.editor.fold.FoldUtilities; import org.netbeans.api.editor.mimelookup.MimeLookup; import org.netbeans.modules.csl.api.OffsetRange; import org.netbeans.modules.csl.api.Severity; @@ -92,7 +96,9 @@ import org.netbeans.modules.parsing.api.Source; import org.netbeans.modules.parsing.api.UserTask; import org.netbeans.modules.parsing.spi.*; -import org.openide.text.NbDocument; +import org.openide.util.NbBundle; + +import static org.netbeans.modules.csl.editor.fold.Bundle.*; /** * This file is originally from Retouche, the Java Support @@ -100,7 +106,23 @@ * as possible to make merging Retouche fixes back as simple as * possible. * - * Copied from both JavaFoldManager and JavaElementFoldManager + * Copied from both JavaFoldManager and JavaElementFoldManager. + *

+ * + * From introduction of {@link FoldType}, this Manager accepts these Strings + * from {@link StructureScanner#folds}:

    + *
  • codes of FoldTypes registered for the language, or + *
  • legacy FoldType codes. + *
+ * The registered FoldTypes take precedence; that is if the SPI returns + * "code-block" and a FoldType is registered with that code, that FoldType (and its FoldTemplate) + * will be used to create the Fold. If no such FoldType exists, the GsfFoldManager will still + * create the fold in a bw compatible mode with an unregistered fold type. But because + * of implementation peculiarities, the FoldType will eventually default to one of the generic + * settings. + *

+ * If a code other than registered FoldTypes and bw compatible constants is used, + * a warning will be printed - to detect programmer errors. * * * @author Jan Lahoda @@ -110,129 +132,60 @@ private static final Logger LOG = Logger.getLogger(GsfFoldManager.class.getName()); - public static final FoldType CODE_BLOCK_FOLD_TYPE = new FoldType("code-block"); // NOI18N - public static final FoldType INITIAL_COMMENT_FOLD_TYPE = new FoldType("initial-comment"); // NOI18N - public static final FoldType IMPORTS_FOLD_TYPE = new FoldType("imports"); // NOI18N - public static final FoldType JAVADOC_FOLD_TYPE = new FoldType("javadoc"); // NOI18N - public static final FoldType TAG_FOLD_TYPE = new FoldType("tag"); // NOI18N - public static final FoldType INNER_CLASS_FOLD_TYPE = new FoldType("inner-class"); // NOI18N - + private static final FoldTemplate TEMPLATE_CODEBLOCK = new org.netbeans.api.editor.fold.FoldTemplate(1, 1, "{...}"); // NOI18N + + /** + * This definition and all Fold types defined in CSL are deprecated and for backward + * compatibility only. + */ + @Deprecated + public static final FoldType CODE_BLOCK_FOLD_TYPE = FoldType.CODE_BLOCK; - private static final String IMPORTS_FOLD_DESCRIPTION = "..."; // NOI18N - - private static final String COMMENT_FOLD_DESCRIPTION = "..."; // NOI18N - - private static final String JAVADOC_FOLD_DESCRIPTION = "..."; // NOI18N - - private static final String CODE_BLOCK_FOLD_DESCRIPTION = "{...}"; // NOI18N - - private static final String TAG_FOLD_DESCRIPTION = "<.../>"; // NOI18N - - - public static final FoldTemplate CODE_BLOCK_FOLD_TEMPLATE - = new FoldTemplate(CODE_BLOCK_FOLD_TYPE, CODE_BLOCK_FOLD_DESCRIPTION, 1, 1); - - public static final FoldTemplate INITIAL_COMMENT_FOLD_TEMPLATE - = new FoldTemplate(INITIAL_COMMENT_FOLD_TYPE, COMMENT_FOLD_DESCRIPTION, 2, 2); - - public static final FoldTemplate IMPORTS_FOLD_TEMPLATE - = new FoldTemplate(IMPORTS_FOLD_TYPE, IMPORTS_FOLD_DESCRIPTION, 0, 0); - - public static final FoldTemplate JAVADOC_FOLD_TEMPLATE - = new FoldTemplate(JAVADOC_FOLD_TYPE, JAVADOC_FOLD_DESCRIPTION, 3, 2); - - public static final FoldTemplate TAG_FOLD_TEMPLATE - = new FoldTemplate(TAG_FOLD_TYPE, TAG_FOLD_DESCRIPTION, 0, 0); - - public static final FoldTemplate INNER_CLASS_FOLD_TEMPLATE - = new FoldTemplate(INNER_CLASS_FOLD_TYPE, CODE_BLOCK_FOLD_DESCRIPTION, 0, 0); - - - public static final String CODE_FOLDING_ENABLE = "code-folding-enable"; //NOI18N - - /** Collapse methods by default - * NOTE: This must be kept in sync with string literal in editor/options - */ - public static final String CODE_FOLDING_COLLAPSE_METHOD = "code-folding-collapse-method"; //NOI18N + @Deprecated + public static final FoldType INITIAL_COMMENT_FOLD_TYPE = FoldType.INITIAL_COMMENT; /** - * Collapse inner classes by default - * NOTE: This must be kept in sync with string literal in editor/options + * Note: this FoldType's code was changed from 'imports' to 'import' to match the used preference key. */ - public static final String CODE_FOLDING_COLLAPSE_INNERCLASS = "code-folding-collapse-innerclass"; //NOI18N + @NbBundle.Messages("FT_label_imports=Imports or Includes") + @Deprecated + public static final FoldType IMPORTS_FOLD_TYPE = FoldType.IMPORT; + + @Deprecated + public static final FoldType JAVADOC_FOLD_TYPE = FoldType.DOCUMENTATION; + + @Deprecated + public static final FoldType TAG_FOLD_TYPE = FoldType.TAG; /** - * Collapse import section default - * NOTE: This must be kept in sync with string literal in editor/options + * This type's code was renamed from inner-class to innerclass to match the preference value */ - public static final String CODE_FOLDING_COLLAPSE_IMPORT = "code-folding-collapse-import"; //NOI18N + @NbBundle.Messages("FT_label_innerclass=Inner Classes") + @Deprecated + public static final FoldType INNER_CLASS_FOLD_TYPE = FoldType.create("innerclass", FT_label_innerclass(), TEMPLATE_CODEBLOCK); - /** - * Collapse javadoc comment by default - * NOTE: This must be kept in sync with string literal in editor/options - */ - public static final String CODE_FOLDING_COLLAPSE_JAVADOC = "code-folding-collapse-javadoc"; //NOI18N - - /** - * Collapse initial comment by default - * NOTE: This must be kept in sync with string literal in editor/options - */ - public static final String CODE_FOLDING_COLLAPSE_INITIAL_COMMENT = "code-folding-collapse-initial-comment"; //NOI18N + @NbBundle.Messages("FT_label_othercodeblocks=Other code blocks") + @Deprecated + public static final FoldType OTHER_CODEBLOCKS_FOLD_TYPE = FoldType.TAG.derive("othercodeblocks", FT_label_othercodeblocks(), + TEMPLATE_CODEBLOCK); - /** - * Collapse tags by default - * NOTE: This must be kept in sync with string literal in editor/options - */ - public static final String CODE_FOLDING_COLLAPSE_TAGS = "code-folding-collapse-tags"; //NOI18N - + private static final Set LEGACY_FOLD_TAGS = new HashSet(11); - protected static final class FoldTemplate { - - private FoldType type; - - private String description; - - private int startGuardedLength; - - private int endGuardedLength; - - protected FoldTemplate(FoldType type, String description, - int startGuardedLength, int endGuardedLength) { - this.type = type; - this.description = description; - this.startGuardedLength = startGuardedLength; - this.endGuardedLength = endGuardedLength; - } - - public FoldType getType() { - return type; - } - - public String getDescription() { - return description; - } - - public int getStartGuardedLength() { - return startGuardedLength; - } - - public int getEndGuardedLength() { - return endGuardedLength; - } - + static { + LEGACY_FOLD_TAGS.add(OTHER_CODEBLOCKS_FOLD_TYPE.code()); + LEGACY_FOLD_TAGS.add(INNER_CLASS_FOLD_TYPE.code()); + LEGACY_FOLD_TAGS.add(TAG_FOLD_TYPE.code()); + LEGACY_FOLD_TAGS.add(JAVADOC_FOLD_TYPE.code()); + LEGACY_FOLD_TAGS.add(IMPORTS_FOLD_TYPE.code()); + LEGACY_FOLD_TAGS.add(INITIAL_COMMENT_FOLD_TYPE.code()); + LEGACY_FOLD_TAGS.add(CODE_BLOCK_FOLD_TYPE.code()); } private FoldOperation operation; private FileObject file; private JavaElementFoldTask task; -// // Folding presets -// private boolean foldImportsPreset; -// private boolean foldInnerClassesPreset; -// private boolean foldJavadocsPreset; -// private boolean foldCodeBlocksPreset; -// private boolean foldInitialCommentsPreset; - private static volatile Preferences prefs; + private volatile Preferences prefs; /** Creates a new instance of GsfFoldManager */ public GsfFoldManager() { @@ -253,7 +206,6 @@ file = DataLoadersBridge.getDefault().getFileObject(doc); if (file != null) { - currentFolds = new HashMap(); task = JavaElementFoldTask.getTask(file); task.setGsfFoldManager(GsfFoldManager.this, file); } @@ -278,7 +230,6 @@ @Override public void removeDamagedNotify(Fold damagedFold) { - currentFolds.remove(operation.getExtraInfo(damagedFold)); if (importsFold == damagedFold) { importsFold = null;//not sure if this is correct... } @@ -297,28 +248,18 @@ task = null; file = null; - currentFolds = null; importsFold = null; initialCommentFold = null; } -// public void settingsChange(SettingsChangeEvent evt) { -// // Get folding presets -// foldInitialCommentsPreset = getSetting(GsfOptions.CODE_FOLDING_COLLAPSE_INITIAL_COMMENT); -// foldImportsPreset = getSetting(GsfOptions.CODE_FOLDING_COLLAPSE_IMPORT); -// foldCodeBlocksPreset = getSetting(GsfOptions.CODE_FOLDING_COLLAPSE_METHOD); -// foldInnerClassesPreset = getSetting(GsfOptions.CODE_FOLDING_COLLAPSE_INNERCLASS); -// foldJavadocsPreset = getSetting(GsfOptions.CODE_FOLDING_COLLAPSE_JAVADOC); -// } - - private static boolean getSetting(String settingName) { - return prefs.getBoolean(settingName, false); - } - static final class JavaElementFoldTask extends IndexingAwareParserResultTask { private final AtomicBoolean cancelled = new AtomicBoolean(false); + private FoldInfo initComment; + + private FoldInfo imports; + public JavaElementFoldTask() { super(TaskIndexingMode.ALLOWED_DURING_SCAN); } @@ -397,7 +338,7 @@ return; } - final TreeSet folds = new TreeSet(); + final Collection folds = new HashSet(); final Document doc = info.getSnapshot().getSource().getDocument(false); if (doc == null) { return; @@ -408,13 +349,16 @@ } if (mgrs instanceof GsfFoldManager) { - SwingUtilities.invokeLater(((GsfFoldManager)mgrs).new CommitFolds(folds, doc, info.getSnapshot().getSource())); + SwingUtilities.invokeLater(((GsfFoldManager)mgrs).createCommit( + folds, + initComment, imports, doc, info.getSnapshot().getSource())); } else { SwingUtilities.invokeLater(new Runnable() { Collection jefms = (Collection)mgrs; public void run() { for (GsfFoldManager jefm : jefms) { - jefm.new CommitFolds(folds, doc, info.getSnapshot().getSource()).run(); + jefm.createCommit(folds, initComment, imports, + doc, info.getSnapshot().getSource()).run(); } }}); } @@ -430,7 +374,7 @@ * * @return true If folds were found, false if cancelled */ - private boolean gsfFoldScan(final Document doc, ParserResult info, final TreeSet folds) { + private boolean gsfFoldScan(final Document doc, ParserResult info, final Collection folds) { final boolean [] success = new boolean [] { false }; Source source = info.getSnapshot().getSource(); @@ -482,7 +426,7 @@ return success[0]; } - private boolean checkInitialFold(final Document doc, final TreeSet folds) { + private boolean checkInitialFold(final Document doc, final Collection folds) { final boolean[] ret = new boolean[1]; ret[0] = true; final TokenHierarchy th = TokenHierarchy.get(doc); @@ -492,52 +436,44 @@ doc.render(new Runnable() { @Override public void run() { - try { - TokenSequence ts = th.tokenSequence(); - if (ts == null) { + TokenSequence ts = th.tokenSequence(); + if (ts == null) { + return; + } + while (ts.moveNext()) { + Token token = ts.token(); + String category = token.id().primaryCategory(); + if ("comment".equals(category)) { // NOI18N + int startOffset = ts.offset(); + int endOffset = startOffset + token.length(); + + // Find end - could be a block of single-line statements + while (ts.moveNext()) { + token = ts.token(); + category = token.id().primaryCategory(); + if ("comment".equals(category)) { // NOI18N + endOffset = ts.offset() + token.length(); + } else if (!"whitespace".equals(category)) { // NOI18N + break; + } + } + + try { + // Start the fold at the END of the line + startOffset = org.netbeans.editor.Utilities.getRowEnd((BaseDocument) doc, startOffset); + if (startOffset >= endOffset) { + return; + } + } catch (BadLocationException ex) { + LOG.log(Level.WARNING, null, ex); + } + + folds.add(initComment = FoldInfo.range(startOffset, endOffset, INITIAL_COMMENT_FOLD_TYPE)); return; } - while (ts.moveNext()) { - Token token = ts.token(); - String category = token.id().primaryCategory(); - if ("comment".equals(category)) { // NOI18N - int startOffset = ts.offset(); - int endOffset = startOffset + token.length(); - boolean collapsed = getSetting(CODE_FOLDING_COLLAPSE_INITIAL_COMMENT); //foldInitialCommentsPreset; - - - // Find end - could be a block of single-line statements - - while (ts.moveNext()) { - token = ts.token(); - category = token.id().primaryCategory(); - if ("comment".equals(category)) { // NOI18N - endOffset = ts.offset() + token.length(); - } else if (!"whitespace".equals(category)) { // NOI18N - break; - } - } - - try { - // Start the fold at the END of the line - startOffset = org.netbeans.editor.Utilities.getRowEnd((BaseDocument) doc, startOffset); - if (startOffset >= endOffset) { - return; - } - } catch (BadLocationException ex) { - LOG.log(Level.WARNING, null, ex); - } - - folds.add(new FoldInfo(doc, startOffset, endOffset, INITIAL_COMMENT_FOLD_TEMPLATE, collapsed)); - return; - } - if (!"whitespace".equals(category)) { // NOI18N - break; - } - + if (!"whitespace".equals(category)) { // NOI18N + break; } - } catch (BadLocationException e) { - ret[0] = false; } } }); @@ -545,7 +481,7 @@ } private void scan(final ParserResult info, - final TreeSet folds, final Document doc, final + final Collection folds, final Document doc, final StructureScanner scanner) { doc.render(new Runnable() { @@ -556,68 +492,82 @@ }); } - private void addFoldsOfType( - StructureScanner scanner, + private boolean addFoldsOfType( String type, Map> folds, - TreeSet result, + Collection result, Document doc, - String collapsedOptionName, - FoldTemplate template) { + FoldType foldType) { List ranges = folds.get(type); //NOI18N if (ranges != null) { - boolean collapseByDefault = getSetting(collapsedOptionName); if (LOG.isLoggable(Level.FINEST)) { - LOG.log(Level.FINEST, "Creating folds {0}, collapsed: {1}", new Object[] { - type, collapseByDefault + LOG.log(Level.FINEST, "Creating folds {0}", new Object[] { + type }); } for (OffsetRange range : ranges) { - try { - if (LOG.isLoggable(Level.FINEST)) { - LOG.log(Level.FINEST, "Fold: {0}", range); - } - addFold(range, result, doc, collapseByDefault, template); - } catch (BadLocationException ble) { - LOG.log(Level.WARNING, "StructureScanner " + scanner + " supplied invalid fold " + range, ble); //NOI18N + if (LOG.isLoggable(Level.FINEST)) { + LOG.log(Level.FINEST, "Fold: {0}", range); } + addFold(range, result, doc, foldType); } + folds.remove(type); + return true; } else { LOG.log(Level.FINEST, "No folds of type {0}", type); + return false; } } - private void addTree(TreeSet result, ParserResult info, Document doc, StructureScanner scanner) { + private void addTree(Collection result, ParserResult info, Document doc, StructureScanner scanner) { // #217322, disabled folding -> no folds will be created - if (!getSetting(CODE_FOLDING_ENABLE)) { + String mime = info.getSnapshot().getMimeType(); + + if (!FoldUtilities.isFoldingEnabled(mime)) { return; } Map> folds = scanner.folds(info); if (cancelled.get()) { return; } - addFoldsOfType(scanner, "codeblocks", folds, result, doc, - CODE_FOLDING_COLLAPSE_METHOD, CODE_BLOCK_FOLD_TEMPLATE); - addFoldsOfType(scanner, "comments", folds, result, doc, - CODE_FOLDING_COLLAPSE_JAVADOC, JAVADOC_FOLD_TEMPLATE); - addFoldsOfType(scanner, "initial-comment", folds, result, doc, - CODE_FOLDING_COLLAPSE_INITIAL_COMMENT, INITIAL_COMMENT_FOLD_TEMPLATE); - addFoldsOfType(scanner, "imports", folds, result, doc, - CODE_FOLDING_COLLAPSE_IMPORT, IMPORTS_FOLD_TEMPLATE); - addFoldsOfType(scanner, "tags", folds, result, doc, - CODE_FOLDING_COLLAPSE_TAGS, TAG_FOLD_TEMPLATE); - addFoldsOfType(scanner, "othercodeblocks", folds, result, doc, - CODE_FOLDING_COLLAPSE_TAGS, CODE_BLOCK_FOLD_TEMPLATE); - addFoldsOfType(scanner, "inner-classes", folds, result, doc, - CODE_FOLDING_COLLAPSE_INNERCLASS, INNER_CLASS_FOLD_TEMPLATE); + Collection ftypes = FoldUtilities.getFoldTypes(mime).values(); + for (FoldType ft : ftypes) { + addFoldsOfType(ft.code(), folds, result, doc, ft); + } + // fallback: transform the legacy keys into FoldInfos + addFoldsOfType("codeblocks", folds, result, doc, + CODE_BLOCK_FOLD_TYPE); + addFoldsOfType("comments", folds, result, doc, + JAVADOC_FOLD_TYPE); + addFoldsOfType("initial-comment", folds, result, doc, + INITIAL_COMMENT_FOLD_TYPE); + addFoldsOfType("imports", folds, result, doc, + IMPORTS_FOLD_TYPE); + addFoldsOfType("tags", folds, result, doc, + TAG_FOLD_TYPE); + addFoldsOfType("othercodeblocks", folds, result, doc, + CODE_BLOCK_FOLD_TYPE); + addFoldsOfType("inner-classes", folds, result, doc, + INNER_CLASS_FOLD_TYPE); + + if (folds.size() > 0) { + LOG.log(Level.WARNING, "Undefined fold types used in {0}: {1}", new Object[] { + info, folds.keySet() + }); + } } - private void addFold(OffsetRange range, TreeSet folds, Document doc, boolean collapseByDefault, FoldTemplate template) throws BadLocationException { + private void addFold(OffsetRange range, Collection folds, Document doc, FoldType type) { if (range != OffsetRange.NONE) { int start = range.getStart(); int end = range.getEnd(); if (start != (-1) && end != (-1) && end <= doc.getLength()) { - folds.add(new FoldInfo(doc, start, end, template, collapseByDefault)); + FoldInfo fi = FoldInfo.range(start, end, type); + // hack for singular imports fold + if (fi.getType() == IMPORTS_FOLD_TYPE && imports == null) { + imports = fi; + } + folds.add(fi); } } } @@ -639,17 +589,25 @@ } + private Runnable createCommit(Collection folds, FoldInfo initComment, FoldInfo imports, Document d, Source s) { + return new CommitFolds(folds, initComment, imports, d, s); + } + private class CommitFolds implements Runnable { - private Document scannedDocument; - private Source scanSource; + private final Document scannedDocument; + private Source scanSource; private boolean insideRender; - private TreeSet infos; + private Collection infos; private long startTime; + private FoldInfo initComment; + private FoldInfo imports; - public CommitFolds(TreeSet infos, Document scanedDocument, Source s) { + public CommitFolds(Collection infos, FoldInfo initComment, FoldInfo imports, Document scannedDocument, Source s) { this.infos = infos; - this.scannedDocument = scanedDocument; + this.initComment = initComment; + this.imports = imports; + this.scannedDocument = scannedDocument; this.scanSource = s; } @@ -658,17 +616,10 @@ * ignores the default state, and takes it from the actual state of * existing fold. */ - private boolean mergeSpecialFoldState(FoldInfo fi) { - if (fi.template == IMPORTS_FOLD_TEMPLATE) { - if (importsFold != null) { - return importsFold.isCollapsed(); - } - } else if (fi.template == INITIAL_COMMENT_FOLD_TEMPLATE) { - if (initialCommentFold != null) { - return initialCommentFold.isCollapsed(); - } + private void mergeSpecialFoldState(FoldInfo fi, Fold f) { + if (fi != null && f != null) { + fi.collapsed(f.isCollapsed()); } - return fi.collapseByDefault; } public void run() { @@ -682,6 +633,9 @@ } operation.getHierarchy().lock(); + if (task == null) { + return; + } if (operation.getHierarchy().getComponent().getDocument() != this.scannedDocument) { Throwable t = (Throwable)scannedDocument.getProperty("Issue-222763-debug"); StringWriter sw = new StringWriter(); @@ -704,71 +658,19 @@ return; } try { - FoldHierarchyTransaction tr = operation.openTransaction(); - try { - if (currentFolds == null) { - return; + mergeSpecialFoldState(imports, importsFold); + mergeSpecialFoldState(initComment, initialCommentFold); + + Map newState = operation.update(infos, null, null); + if (imports != null) { + importsFold = newState.get(imports); } - - Map added = new TreeMap(); - // TODO - use map duplication here instead? - TreeSet removed = new TreeSet(currentFolds.keySet()); - int documentLength = document.getLength(); - - for (FoldInfo i : infos) { - if (removed.remove(i)) { - continue ; - } - - int start = i.start.getOffset(); - int end = i.end.getOffset(); - - if (end > documentLength) { - continue; - } - - if (end > start && (end - start) > (i.template.getStartGuardedLength() + i.template.getEndGuardedLength())) { - Fold f = operation.addToHierarchy(i.template.getType(), - i.template.getDescription(), - mergeSpecialFoldState(i), - start, - end, - i.template.getStartGuardedLength(), - i.template.getEndGuardedLength(), - i, - tr); - - added.put(i, f); - - if (i.template == IMPORTS_FOLD_TEMPLATE) { - importsFold = f; - } - if (i.template == INITIAL_COMMENT_FOLD_TEMPLATE) { - initialCommentFold = f; - } - } + if (initComment != null) { + initialCommentFold = newState.get(initComment); } - - for (FoldInfo i : removed) { - Fold f = currentFolds.remove(i); - - operation.removeFromHierarchy(f, tr); - - if (importsFold == f ) { - importsFold = null; - } - - if (initialCommentFold == f) { - initialCommentFold = f; - } - } - - currentFolds.putAll(added); } catch (BadLocationException e) { LOG.log(Level.WARNING, null, e); - } finally { - tr.commit(); } } finally { operation.getHierarchy().unlock(); @@ -781,63 +683,8 @@ } } - private Map currentFolds; private Fold initialCommentFold; private Fold importsFold; - - protected static final class FoldInfo implements Comparable { - - private Position start; - private Position end; - private FoldTemplate template; - private boolean collapseByDefault; - - public FoldInfo(Document doc, int start, int end, FoldTemplate template, boolean collapseByDefault) throws BadLocationException { - this.start = doc.createPosition(start); - // see issue #216378; while Fold.end Position is manually updated by FoldHierarchyTransactionImpl, the - // FoldInfos are left alone, and end marker must stick with the content, so characters typed after it does - // not extend the FoldInfo - this.end = NbDocument.createPosition(doc, end, Position.Bias.Backward); - this.template = template; - this.collapseByDefault = collapseByDefault; - } - - @Override - public int hashCode() { - return 1; - } - - @Override - public boolean equals(Object o) { - if (!(o instanceof FoldInfo)) { - return false; - } - - return compareTo(o) == 0; - } - - public int compareTo(Object o) { - FoldInfo remote = (FoldInfo) o; - - if (start.getOffset() < remote.start.getOffset()) { - return -1; - } - - if (start.getOffset() > remote.start.getOffset()) { - return 1; - } - - if (end.getOffset() < remote.end.getOffset()) { - return -1; - } - - if (end.getOffset() > remote.end.getOffset()) { - return 1; - } - - return 0; - } - } private static boolean hasErrors(ParserResult r) { for(org.netbeans.modules.csl.api.Error e : r.getDiagnostics()) { diff --git a/editor.codetemplates/nbproject/org-netbeans-modules-editor-codetemplates.sig b/editor.codetemplates/nbproject/org-netbeans-modules-editor-codetemplates.sig --- a/editor.codetemplates/nbproject/org-netbeans-modules-editor-codetemplates.sig +++ b/editor.codetemplates/nbproject/org-netbeans-modules-editor-codetemplates.sig @@ -1,5 +1,5 @@ #Signature file v4.1 -#Version 1.26.1 +#Version 1.31.0 CLSS public java.lang.Object cons public init() diff --git a/editor.codetemplates/nbproject/project.properties b/editor.codetemplates/nbproject/project.properties --- a/editor.codetemplates/nbproject/project.properties +++ b/editor.codetemplates/nbproject/project.properties @@ -45,6 +45,6 @@ #javadoc.name=EditorCodeTemplates javadoc.apichanges=${basedir}/apichanges.xml javadoc.arch=${basedir}/arch.xml -spec.version.base=1.31.0 +spec.version.base=1.32.0 test.config.stableBTD.includes=**/*Test.class diff --git a/editor.codetemplates/nbproject/project.xml b/editor.codetemplates/nbproject/project.xml --- a/editor.codetemplates/nbproject/project.xml +++ b/editor.codetemplates/nbproject/project.xml @@ -99,7 +99,7 @@ 1 - 1.20 + 1.31 diff --git a/editor.codetemplates/src/org/netbeans/lib/editor/codetemplates/storage/SettingsProvider.java b/editor.codetemplates/src/org/netbeans/lib/editor/codetemplates/storage/SettingsProvider.java --- a/editor.codetemplates/src/org/netbeans/lib/editor/codetemplates/storage/SettingsProvider.java +++ b/editor.codetemplates/src/org/netbeans/lib/editor/codetemplates/storage/SettingsProvider.java @@ -106,11 +106,11 @@ this.ic = ic; // Start listening - MimePath [] allPaths = computeInheritedMimePaths(mimePath); - this.allCtsi = new CodeTemplateSettingsImpl[allPaths.length]; + List allPaths = mimePath.getIncludedPaths(); + this.allCtsi = new CodeTemplateSettingsImpl[allPaths.size()]; - for(int i = 0; i < allPaths.length; i++) { - this.allCtsi[i] = CodeTemplateSettingsImpl.get(allPaths[i]); + for(int i = 0; i < allPaths.size(); i++) { + this.allCtsi[i] = CodeTemplateSettingsImpl.get(allPaths.get(i)); this.allCtsi[i].addPropertyChangeListener(WeakListeners.propertyChange(this, this.allCtsi[i])); } } @@ -170,30 +170,4 @@ } } // End of CompositeCTS class - - private static MimePath [] computeInheritedMimePaths(MimePath mimePath) { - List paths = null; - try { - Method m = MimePath.class.getDeclaredMethod("getInheritedPaths", String.class, String.class); //NOI18N - m.setAccessible(true); - @SuppressWarnings("unchecked") - List ret = (List) m.invoke(mimePath, null, null); - paths = ret; - } catch (Exception e) { - LOG.log(Level.WARNING, "Can't call org.netbeans.api.editor.mimelookup.MimePath.getInheritedPaths method.", e); //NOI18N - } - - if (paths != null) { - ArrayList mimePaths = new ArrayList(paths.size()); - - for (String path : paths) { - mimePaths.add(MimePath.parse(path)); - } - - return mimePaths.toArray(new MimePath[mimePaths.size()]); - } else { - return new MimePath [] { mimePath, MimePath.EMPTY }; - } - } - } diff --git a/editor.fold/arch.xml b/editor.fold/arch.xml --- a/editor.fold/arch.xml +++ b/editor.fold/arch.xml @@ -450,6 +450,41 @@ } +

Updating Fold hierarchy

+In the preceding cases, maintaining Folds was the FoldManager's responsibility. The FoldManager typically +held a copy of the Folds added to the hierarchy, and during the refresh, it compared them to the new data +and decided what folds to remove. +For simple cases, which only create/remove folds based on text positions, part of the work can be offloaded to the +FoldOperation: + +
+    // create new fold positional information for all folds.
+    Collection<FoldInfo> newInfos = ...;
+    
+    // create FoldInfo for each of the fold
+    newInfos.add(
+        FoldInfo.range(start, end, type).
+            withTemplate(customTemplate).
+            withDescription(veryCustomDescription).
+            collapse(true)
+    );
+    
+    // the hierarchy must be locked prior to update
+    
+    doc.readLock();
+    hierarchy.lock();
+    try {
+        operation.update(newInfos, null, null);
+    } finally {
+    }
+
+ +The update() operation performs a diff, creates new folds, discards old ones, and updates the folds, which +prevailed. + +

Accessing folds

+Instead of keeping a copy of created folds, the FoldManager may call operation.foldIterator. The iterator +will enumerate all folds, including (recursively) blocked ones. @@ -1204,4 +1239,85 @@ No. + + + + + + + + + + + + +

+ XXX no answer for compat-deprecation +

+
+ + + + + +

+ XXX no answer for exec-ant-tasks +

+
+ + + + + +

+ XXX no answer for resources-preferences +

+
+ diff --git a/editor.fold/nbproject/org-netbeans-modules-editor-fold.sig b/editor.fold/nbproject/org-netbeans-modules-editor-fold.sig --- a/editor.fold/nbproject/org-netbeans-modules-editor-fold.sig +++ b/editor.fold/nbproject/org-netbeans-modules-editor-fold.sig @@ -1,5 +1,5 @@ #Signature file v4.1 -#Version 1.29.2 +#Version 1.33.0 CLSS public abstract interface java.io.Serializable diff --git a/editor.fold/nbproject/project.properties b/editor.fold/nbproject/project.properties --- a/editor.fold/nbproject/project.properties +++ b/editor.fold/nbproject/project.properties @@ -46,6 +46,6 @@ #cp.extra= javac.source=1.6 -spec.version.base=1.33.0 +spec.version.base=1.34.0 test.config.stableBTD.includes=**/*Test.class diff --git a/editor.fold/nbproject/project.xml b/editor.fold/nbproject/project.xml --- a/editor.fold/nbproject/project.xml +++ b/editor.fold/nbproject/project.xml @@ -50,6 +50,15 @@ org.netbeans.modules.editor.fold + org.netbeans.modules.editor.lib + + + + 3 + 3.33 + + + org.netbeans.modules.editor.lib2 @@ -64,7 +73,7 @@ 1 - 1.20 + 1.31 @@ -77,6 +86,15 @@ + org.netbeans.modules.editor.settings.storage + + + + 1 + 1.38 + + + org.netbeans.modules.editor.util @@ -85,6 +103,32 @@ + org.netbeans.modules.lexer + + + + 2 + 1.49 + + + + org.netbeans.modules.options.api + + + + 1 + 1.31 + + + + org.openide.awt + + + + 7.55 + + + org.openide.filesystems @@ -147,8 +191,12 @@ org.netbeans.api.editor.fold + org.netbeans.editor org.netbeans.spi.editor.fold + + initially + diff --git a/editor.fold/src/org/netbeans/api/editor/fold/Fold.java b/editor.fold/src/org/netbeans/api/editor/fold/Fold.java --- a/editor.fold/src/org/netbeans/api/editor/fold/Fold.java +++ b/editor.fold/src/org/netbeans/api/editor/fold/Fold.java @@ -48,6 +48,7 @@ import javax.swing.text.BadLocationException; import javax.swing.text.Document; import javax.swing.text.Position; +import org.netbeans.modules.editor.fold.ApiPackageAccessor; import org.netbeans.modules.editor.fold.FoldOperationImpl; import org.netbeans.modules.editor.fold.FoldChildren; import org.netbeans.modules.editor.fold.FoldUtilitiesImpl; @@ -108,7 +109,6 @@ private Object extraInfo; - Fold(FoldOperationImpl operation, FoldType type, String description, boolean collapsed, Document doc, int startOffset, int endOffset, @@ -404,6 +404,40 @@ public int getFoldIndex(Fold child) { return (children != null) ? children.getFoldIndex(child) : -1; } + + /** + * Start of the fold content past the guarded area. + * For root fold, returns 0. For other folds, returns the offset + * following the guarded start of the fold (typically a delimiter). + * + * @return offset in document + */ + public int getGuardedStart() { + if (isRootFold()) { + return 0; + } else if (isZeroStartGuardedLength()) { + return getStartOffset(); + } else { + return getStartOffset() + startGuardedLength; + } + } + + /** + * End of the fold content before the guarded area. + * For root fold, returns 0. For other folds, returns the last offset + * preceding the guarded start of the fold (typically a delimiter). + * + * @return offset in document + */ + public int getGuardedEnd() { + if (isRootFold()) { + return getEndOffset(); + } else if (isZeroStartGuardedLength()) { + return getEndOffset(); + } else { + return getEndOffset() - endGuardedLength; + } + } private void updateGuardedStartPos(Document doc, int startOffset) throws BadLocationException { if (!isRootFold()) { @@ -530,7 +564,6 @@ this.rawIndex += rawIndexDelta; } - public String toString() { return FoldUtilitiesImpl.foldToString(this) + ", [" + getInnerStartOffset() // NOI18N + ", " + getInnerEndOffset() + "] {" // NOI18N diff --git a/editor.fold/src/org/netbeans/api/editor/fold/FoldHierarchy.java b/editor.fold/src/org/netbeans/api/editor/fold/FoldHierarchy.java --- a/editor.fold/src/org/netbeans/api/editor/fold/FoldHierarchy.java +++ b/editor.fold/src/org/netbeans/api/editor/fold/FoldHierarchy.java @@ -53,6 +53,7 @@ import org.netbeans.modules.editor.fold.ApiPackageAccessor; import org.netbeans.modules.editor.fold.FoldHierarchyExecution; import org.netbeans.modules.editor.fold.FoldOperationImpl; +import org.netbeans.spi.editor.fold.FoldTypeProvider; /** * Hierarchy of the folds for a single text component represents @@ -122,7 +123,7 @@ static { ensureApiAccessorRegistered(); } - + private static void ensureApiAccessorRegistered() { if (!apiPackageAccessorRegistered) { apiPackageAccessorRegistered = true; @@ -458,6 +459,10 @@ fsc.endOffsetChanged(originalEndOffset); } + public FoldHierarchyExecution foldGetExecution(FoldHierarchy h) { + return h.execution; + } + } } diff --git a/editor.fold/src/org/netbeans/api/editor/fold/FoldTemplate.java b/editor.fold/src/org/netbeans/api/editor/fold/FoldTemplate.java new file mode 100644 --- /dev/null +++ b/editor.fold/src/org/netbeans/api/editor/fold/FoldTemplate.java @@ -0,0 +1,125 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2011 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + * + * Contributor(s): + * + * Portions Copyrighted 2011 Sun Microsystems, Inc. + */ +package org.netbeans.api.editor.fold; + +import org.openide.util.NbBundle; + +import static org.netbeans.api.editor.fold.Bundle.*; + +/** + * Template that describes how the fold should appear and interact with the + * user. The FoldTemplate defines how many characters at the start (end) should + * act as a fold guard: change to that area will remove the fold because it + * becomes damaged. It also defines what placeholder should appear in place of the + * folded code. + *

+ * A template may be attached to a {@link FoldType} instance. Folds of that kind will + * automatically use the attached Template, if not overriden explicitly. + *

+ * The string {@code "..."} ({@link #CONTENT_PLACEHOLDER}) is treated as a placeholder. If a + * ContentReader is available for the {@link FoldType}, the placeholder will be replaced by + * the product of the Reader. Otherwise the placeholder will remain in the displayText + * and will be presented. + * + * @since 1.34 + * @author sdedic + */ +public final class FoldTemplate { + /** + * The default template for folded text: no markers before+after, ellipsis + * shown. + */ + @NbBundle.Messages("FT_DefaultTemplate=...") + public static final FoldTemplate DEFAULT = new FoldTemplate(0, 0, FT_DefaultTemplate()); // NOI18N + + /** + * A standard template, which represents a block of something. + */ + @NbBundle.Messages("FT_DefaultBlockTemplate={...}") + public static final FoldTemplate DEFAULT_BLOCK = new FoldTemplate(0, 0, FT_DefaultBlockTemplate()); // NOI18N + + /** + * This string is interpreted as a placeholder for the infrastructure to inject + */ + public static final String CONTENT_PLACEHOLDER = FT_DefaultTemplate(); + + /** + * # of characters at the start of the fold, which serve as a marker that the fold + * has been damaged/destroyed + */ + private int guardedStart; + + /** + * The guarded portion at the end of the fold + */ + private int guardedEnd; + + /** + * Description that appears in the folded area. + */ + private String displayText; + + /** + * Creates a FoldTemplate with a fixed description. + * + * @param guardedStart length of the start marker, or -1 if no start marker is present + * @param guardedEnd length of the end marker, or -1 if no end marker is present + * @param displayText text which should be displayed in place of the folded content + */ + public FoldTemplate(int guardedStart, int guardedEnd, String displayText) { + this.guardedStart = guardedStart; + this.guardedEnd = guardedEnd; + this.displayText = displayText; + } + + public String getDescription() { + return displayText; + } + + public int getGuardedEnd() { + return guardedEnd; + } + + public int getGuardedStart() { + return guardedStart; + } +} diff --git a/editor.fold/src/org/netbeans/api/editor/fold/FoldType.java b/editor.fold/src/org/netbeans/api/editor/fold/FoldType.java --- a/editor.fold/src/org/netbeans/api/editor/fold/FoldType.java +++ b/editor.fold/src/org/netbeans/api/editor/fold/FoldType.java @@ -44,48 +44,339 @@ package org.netbeans.api.editor.fold; +import java.util.Collection; +import javax.swing.event.ChangeListener; +import org.openide.util.NbBundle; +import org.openide.util.Parameters; + +import static org.netbeans.api.editor.fold.Bundle.*; + /** * Each {@link Fold} is identified by its fold type. *
* Each fold type presents a fold type acceptor as well * accepting just itself. * - *

+ *

+ * * As the equals() method is declared final * (delegating to super) the fold types * can directly be compared by using == * operator. + * + *

+ * FoldTypes are MIME-type specific. Sets of FoldType, which apply + * to a certain MIME type are collected in a {@link FoldType.Domain}, + * which can be obtained by {@link FoldUtilities#getFoldTypes}. FoldTypes for a MIME type are + * collected with the help of {@link org.netbeans.spi.editor.fold.FoldTypeProvider}. + *

+ * Several generalized types of folds are defined in this class as constants. + * When creating custom FoldTypes, please check carefully whether the new FoldType is not, + * in fact, one of these general ones, + * or is not a specialization of it. If a general action (e.g. auto-collapse all folds XXX) would + * make sense for the new fold, consider to {@link #derive} it from an existing one. + *

+ * Each FoldType has a specific FoldTemplate instance that controls how it is presented. If a + * new Fold has same semantics as an existing one, but a different {@link FoldTemplate} is needed, + * use {@link #override} to create a new FoldType with appropriate properties. FoldTypes + * defined here can be reused in individual MIME types, if their configuration is appropriate. + * In general, FoldTypes from more general MIME types (e.g. text/xml) can be reused in specialized ones + * (e.g. text/x-ant+xml). + *

+ * FoldTypes can form a hierarchy of generalization. For example java Method fold can have a more general + * 'parent', {@link #MEMBER}. + *

+ * The generalization is designed to allow several kinds of the same concept operated separately at the + * level of the language, but managed at once using general options or operations. + * If no settings are specified for Method folding, {@link #MEMBER} settings are + * applied. Likewise, a general-purpose folding operation can work on {@link #MEMBER} fold type, + * which will affect both FUNCTION in PHP or METHOD in java (and other fold types in other languages). + *

+ * Because of this, please be careful + * to use {@link #isKindOf} to check whether a FoldType is of a specific type. This method respects the + * generalization. == can be still used to check for exact type match. When using ==, javadoc comments + * may be evaluated as different from general 'documentation' and yet different from PHP docs. Comparison using + * == are safe only when applied on FoldType instances defined for the same MIME type +*

* - *

- * Fold providers should export all the fold types - * that they provide in their APIs so that the - * clients can use these fold types in operations - * with the fold hierarchy. - * + * @author sdedic * @author Miloslav Metelka * @version 1.00 */ public final class FoldType { + /** + * A generic code block. Note that other categories are defined that represent + * method, class, member or nested symbol levels + * @since 1.34 + */ + @NbBundle.Messages({ + "FT_Label_code-block=Code Blocks", + "FT_display_code-block={...}", + "FT_display_default=..." + }) + public static final FoldType CODE_BLOCK = create("code-block", FT_Label_code_block(), + new FoldTemplate(1, 1, FT_display_code_block())); + + /** + * Documentation comment, as defined by the language + * @since 1.34 + */ + @NbBundle.Messages("FT_Label_javadoc=Documentation") + public static final FoldType DOCUMENTATION = create("documentation", FT_Label_javadoc(), + FoldTemplate.DEFAULT); - private final String description; + /** + * Unspecified type of comment + * @since 1.34 + */ + @NbBundle.Messages("FT_Label_comment=Comments") + public static final FoldType COMMENT = create("comment", FT_Label_comment(), + FoldTemplate.DEFAULT); /** + * Initial file-level comment. Either documentation comment or comment containing + * copyright. Defaults to 'comment' + * @since 1.34 + */ + @NbBundle.Messages("FT_Label_initial-comment=Initial Comment") + public static final FoldType INITIAL_COMMENT = create("initial-comment", FT_Label_initial_comment(), + FoldTemplate.DEFAULT); + + /** + * Tag in markup languages + * @since 1.34 + */ + @NbBundle.Messages({ + "FT_Label_tag=Tags and Markup", + "FT_display_tag=<.../>" + }) + public static final FoldType TAG = create("tag", FT_Label_tag(), + new FoldTemplate(1, 2, FT_display_tag())); + + /** + * Nested part; for example an embedded type, or nested namespace. Do not use + * this category for top-level symbols. This type is different from a 'member', + * as type may have many member types, nested types form worlds of their own + * @since 1.34 + */ + @NbBundle.Messages("FT_Label_inner-class=Nested Blocks") + public static final FoldType NESTED = create("nested", FT_Label_inner_class(), + FoldTemplate.DEFAULT); + + /** + * Member of a symbol (type, class, object) + * @since 1.34 + */ + @NbBundle.Messages("FT_Label_member=Member") + public static final FoldType MEMBER = create("member", FT_Label_member(), + FoldTemplate.DEFAULT); + + /** + * Various import, includes and references to sibling files. + * + * @since 1.34 + */ + @NbBundle.Messages("FT_Label_import=Imports and Includes") + public static final FoldType IMPORT = create("import", FT_Label_import(), + FoldTemplate.DEFAULT); + + /** + * User-defined fold, recoded using a special comment + * @since 1.34 + */ + @NbBundle.Messages("FT_Label_user-defined=User defined") + public static final FoldType USER = create("user", FT_Label_user_defined(), null); + + /** * Construct fold type with the given description. * * @param description textual description of the fold type. * Two fold types with the same description are not equal. + * @deprecated Use the {link create} static method. */ + @Deprecated public FoldType(String description) { - this.description = description; + this(description, null, null, null); } - public boolean accepts(FoldType type) { - return (type == this); + private FoldType(String code, String label, FoldTemplate template, FoldType parent) { + this.code = code; + this.parent = parent; + this.label = label; + this.template = template != null ? template : FoldTemplate.DEFAULT; + } + + /** + * Creates an instance of FoldType. + * The code, label and template parameters will be assigned to the FoldType's properties. + * + * @param code code used to form, e.g. persistent keys for info related to the FoldType + * @param label human-readable label that identifies the FoldType + * @param template describes how the fold is presented + * @return FoldType instance + * @since 1.34 + */ + public static FoldType create(String code, String label, FoldTemplate template) { + Parameters.notWhitespace("code", code); + Parameters.notWhitespace("label", label); + return new FoldType(code, label, template, null); } + /** + * Creates a FoldType, overriding its appearance. + * The new FoldType will use the same code, but label and/or template can be changed. + * + * @param label human-readable label that describes the FoldType + * @param template human-readable label that describes the FoldType + * @return an instance of FoldType initialized according to parameters + * @since 1.34 + */ + public FoldType override(String label, FoldTemplate template) { + return derive(code(), label, template); + } + + /** + * Derives a FoldType which acts as a child of this instance. + * The FoldType will be treated as a special case of this instance. If A is the returned + * instance and B is this instance, then A.isKindOf(B) will return true. + *

+ * + * @param code new code for the FoldType + * @param label human-readable label that describes the FoldType + * @param template human-readable label that describes the FoldType + * @return an instance of child FoldType + * @since 1.34 + */ + public FoldType derive(String code, String label, FoldTemplate template) { + Parameters.notWhitespace("code", code); + Parameters.notWhitespace("label", label); + return new FoldType(code, label, template, this); + } + + /** + * @param type + * @return + * @deprecated Use {@link #isKindOf} + */ + @Deprecated + public boolean accepts(FoldType type) { + return isKindOf(type); + } + + @Override public String toString() { - return description; + return code(); + } + + /** + * Provides human-readable label for the type + * @return + * @since 1.34 + */ + public String getLabel() { + return label; + } + + /** + * Provides FoldTemplate instance for this type. + * The FoldTemplate is the default template used when creating a Fold. Specific FoldTemplate + * may be provided when creating the Fold instance. + * + * @return fold template for the type. + */ + public FoldTemplate getTemplate() { + return template; + } + + /** + * Checks whether the fold can act as the 'other' type. + * This check respect parent relationship (see {@link #parent}). The method returns true, + * if semantics, operations, settings,... applicable to 'other' could be also applied on + * this FoldType. + * + * @param other the type to check + * @return true, if this FoldType should be affected + * @since 1.34 + */ + public boolean isKindOf(FoldType other) { + return other == this || parent != null && parent.isKindOf(other); + } + + /** + * Provides parent of this FoldType. Returns {@code null}, if no parent is defined. + * The parent is a FoldType, which is more general and could be used as a fallback when no information + * is provided/available on this FoldType instance. + * + * @return parent instance or {@code null} + * @since 1.34 + */ + public FoldType parent() { + return parent; + } + + /** + * Persistable value of FoldType. + * Use {@link FoldType.Domain#valueOf} to convert the value back to FoldType instance. The value + * can (and is) used to compose preference setting keys. + * + * @return value of FoldType. + * @since 1.34 + */ + public String code() { + return code; } + /** + * Represents a value set of {@link FoldType}s for one MIME type. + * The instance collects all FoldTypes defined for a certain MIME type. "" mime type + * represents 'global' FoldTypes. + *

+ * The instance will fire change events when the set of fold types change, e.g. as a result + * of enabled/disabled modules (appearance of {@link FoldTypeProvider}). + * @since 1.34 + */ + public interface Domain { + /** + * Enumerates FoldTypes. Returns empty collection, if no fold types are known. + * @return fold types. + */ + Collection values(); + + /** + * Translates String code into FoldType. Returns null if the fold type is not found. + * The String should have been obtained by a call to FoldType.code(). + * + * @param s code value + * @return the FoldType instance. + */ + FoldType valueOf(String s); + + /** + * Attaches listener to be notified when set of FoldTypes change. + * + * @param l listener instance. + */ + void addChangeListener(ChangeListener l); + + /** + * Detaches the change listener + * @param l listener instance. + */ + void removeChangeListener(ChangeListener l); + } + + private final String code; + + private final FoldType parent; + + /** + * Display name for the fold type, for presentation in UI + */ + private final String label; + + /** + * The default fold template + */ + private final FoldTemplate template; } diff --git a/editor.fold/src/org/netbeans/api/editor/fold/FoldUtilities.java b/editor.fold/src/org/netbeans/api/editor/fold/FoldUtilities.java --- a/editor.fold/src/org/netbeans/api/editor/fold/FoldUtilities.java +++ b/editor.fold/src/org/netbeans/api/editor/fold/FoldUtilities.java @@ -48,6 +48,8 @@ import java.util.Collections; import java.util.Iterator; import java.util.List; +import org.netbeans.api.editor.mimelookup.MimePath; +import org.netbeans.modules.editor.fold.FoldRegistry; import org.netbeans.modules.editor.fold.FoldUtilitiesImpl; /** @@ -440,4 +442,41 @@ return FoldUtilitiesImpl.collapsedFoldIterator(hierarchy, startOffset, endOffset); } + /** + * Obtains available FoldType values for the specified MIME type. + * + * @param mimeType mime type of the content + * @return available FoldTypes + * @since 1.34 + */ + public static FoldType.Domain getFoldTypes(String mimeType) { + return FoldRegistry.get().getDomain(MimePath.parse(mimeType)); + } + + /** + * Determines whether folds of that type are should be initially collapsed. + * The FoldType is evaluated in the context of a specific FoldHierarchy, that is a Component + * with a content of a certain MIME type. + * + * @param ft FoldType to inspect + * @param hierarchy context for evaluation + * @return true, if folds of FoldType should be initially collapsed. + * @since 1.34 + */ + public static boolean isAutoCollapsed(FoldType ft, FoldHierarchy hierarchy) { + return FoldUtilitiesImpl.isAutoCollapsed(ft, hierarchy); + } + + /** + * Determines whether folding is enabled for a given MIME type. + * Use {@link MimePath#EMPTY}.getMimeType() to query for the default (all languages) + * setting. + * + * @param mimeType the MIME type of the content. + * @return true, if folding is enabled, false otherwise. + * @since 1.34 + */ + public static boolean isFoldingEnabled(String mimeType) { + return FoldUtilitiesImpl.isFoldingEnabled(mimeType); + } } diff --git a/editor.fold/src/org/netbeans/api/editor/fold/FoldingSupport.java b/editor.fold/src/org/netbeans/api/editor/fold/FoldingSupport.java new file mode 100644 --- /dev/null +++ b/editor.fold/src/org/netbeans/api/editor/fold/FoldingSupport.java @@ -0,0 +1,194 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2013 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + * + * Contributor(s): + * + * Portions Copyrighted 2013 Sun Microsystems, Inc. + */ +package org.netbeans.api.editor.fold; + +import java.util.Map; +import javax.swing.JComponent; +import javax.swing.text.JTextComponent; +import org.netbeans.editor.SideBarFactory; +import org.netbeans.modules.editor.fold.CustomFoldManager; +import org.netbeans.modules.editor.fold.JavadocReader; +import org.netbeans.modules.editor.fold.ui.CodeFoldingSideBar; +import org.netbeans.spi.editor.fold.ContentReader; +import org.netbeans.spi.editor.fold.FoldManager; +import org.netbeans.spi.editor.fold.FoldManagerFactory; +import org.openide.util.Parameters; + +/** + * This utility class collects APIs to create default implementations + * of various pieces of infrastructure. + * + * @author sdedic + */ +public final class FoldingSupport { + private static SideBarFactory FACTORY = null; + + private FoldingSupport() {} + + /** + * Creates a default implementation of {@link ContentReader}. + * + * The default implementation is modeled to work with Javadoc-like comments. It will + * ignore markers at line start (first non-whitespace) - 'start' parameter. If the reader + * encounters the 'stop' regex pattern, it stops scanning and returns {@code null}. Typically + * some tags are placed at the end of the doc comment, and they are not informative enough + * to put them into folded preview. + *

+ * Finally, if a suitable content is found, and it contains the 'terminator' pattern, + * the content is only returned up to (excluding) the terminator. + *

+ * Javadoc (PHPdoc) reader can be constructed as defaultReader("*", "\\.", "@"); + * + * @param start character sequence, which will be ignored at the beginning of the line. Can be {@code null} + * @param terminator pattern, which marks the end of the summary line/sentence. Can be {@code null} + * @param stop content search stop at the 'stop' pattern. Can be {@code null} + */ + public static ContentReader contentReader(String start, String terminator, String stop, String prefix) { + return new JavadocReader(start, terminator, stop, prefix); + } + + /** + * A variant of {@link #defaultReader} usable from FS layer. + * See {@link #defaultReader} documentation for parameter explanation. The + * method expects those parameters as keys in the Map (values are all Strings). + * An additional entry (key: 'type') is required in the map, it identifies the + * FoldType for which the reader should work. + * + * @param m configuration parameters for the reader factory. + * @see #contentReader + */ + public static ContentReader.Factory contentReaderFactory(Map m) { + final String start = (String) m.get("start"); // NOI18N + final String terminator = (String) m.get("terminator"); // NOI18N + final String stop = (String) m.get("stop"); // NOI18N + final String foldType = (String) m.get("type"); // NOI18N + final String prefix = (String)m.get("prefix"); // NOI18N + return new ContentReader.Factory() { + @Override + public ContentReader createReader(FoldType ft) { + if (foldType != null && foldType.equals(ft.code()) || (foldType == null && ft.isKindOf(FoldType.DOCUMENTATION))) { + return contentReader(start, terminator, stop, prefix); + } else { + return null; + } + } + }; + } + + /** + * Creates a component for the code folding sidebar. + * Returns a standard folding sidebar component, which displays code folds. This sidebar implementation + * can be created only once per text component. + * + * @param textComponent the text component which should work with the Sidebar + * @return Sidebar instance. + */ + public static JComponent sidebarComponent(JTextComponent textComponent) { + return new CodeFoldingSideBar(textComponent); + } + + /** + * Creates a user-defined fold manager, that processes specific token type. + * The manager tries to find start/end markers within the token text. If found, + * a Fold is created. The manager only looks in tokens, whose {@code primaryCategory} + * starts with a String, which is stored under 'tokenId' key in the params map. + *

+ * The method is designed to be called from the filesystem layer as follows: + *

+     * <file name="my-custom-foldmanager.instance">
+     * <attr name="instanceCreate" methodvalue="org.netbeans.api.editor.fold.FoldUtilities.createUserFoldManager"/>
+     * <attr name="tokenid" stringvalue="comment"/>
+     * </file>
+     * 
+ * + * @param map the configuration parameters. + * @return FoldManagerFactory instance + */ + public static FoldManagerFactory userFoldManagerFactory(Map params) { + final String s = (String) params.get("tokenId"); + return new FoldManagerFactory() { + @Override + public FoldManager createFoldManager() { + return userFoldManager(s); + } + }; + } + + /** + * Creates a user-defined fold manager, that processes specific token type. + * The manager tries to find start/end markers within the token text. If found, + * a Fold is created. The manager only looks in tokens, whose {@code primaryCategory} + * starts with tokenId string. + *

+ * {@code Null} value of 'tokenId' means the default "comment" will be used. + * + * @param tokenId filter for prefix of the token's primaryCategory. + * @return FoldManager instance + */ + public static FoldManager userFoldManager(String tokenId) { + if (tokenId != null) { + return new CustomFoldManager(tokenId); + } else { + return new CustomFoldManager(); + } + } + + /** + * Obtains an instance of folding sidebar factory. This method should + * be used in layer, in the MIME lookup area, to register a sidebar with + * an editor for a specific MIMEtype. + *

+ * There's a default sidebar instance registered for all MIME types. + * + * @return shared sidebar factory instance + */ + public static SideBarFactory foldingSidebarFactory() { + if (FACTORY != null) { + return FACTORY; + } + return FACTORY = new CodeFoldingSideBar.Factory(); + } + + public static void disableCodeFoldingSidebar(JTextComponent text) { + text.putClientProperty(CodeFoldingSideBar.PROP_SIDEBAR_MARK, true); + } +} diff --git a/editor.fold/src/org/netbeans/editor/CodeFoldingSideBar.java b/editor.fold/src/org/netbeans/editor/CodeFoldingSideBar.java new file mode 100644 --- /dev/null +++ b/editor.fold/src/org/netbeans/editor/CodeFoldingSideBar.java @@ -0,0 +1,1482 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 1997-2010 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * Contributor(s): + * + * The Original Software is NetBeans. The Initial Developer of the Original + * Software is Sun Microsystems, Inc. Portions Copyright 1997-2007 Sun + * Microsystems, Inc. All Rights Reserved. + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + */ + +package org.netbeans.editor; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.FontMetrics; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.Stroke; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.NavigableMap; +import java.util.TreeMap; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.prefs.PreferenceChangeEvent; +import java.util.prefs.PreferenceChangeListener; +import java.util.prefs.Preferences; +import javax.accessibility.Accessible; +import javax.accessibility.AccessibleContext; +import javax.accessibility.AccessibleRole; +import javax.swing.JComponent; +import javax.swing.SwingUtilities; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import javax.swing.text.AbstractDocument; +import javax.swing.text.AttributeSet; +import javax.swing.text.BadLocationException; +import javax.swing.text.Document; +import javax.swing.text.JTextComponent; +import javax.swing.text.View; +import org.netbeans.api.editor.fold.Fold; +import org.netbeans.api.editor.fold.FoldHierarchy; +import org.netbeans.api.editor.fold.FoldHierarchyEvent; +import org.netbeans.api.editor.fold.FoldHierarchyListener; +import org.netbeans.api.editor.fold.FoldUtilities; +import org.netbeans.api.editor.mimelookup.MimeLookup; +import org.netbeans.api.editor.settings.AttributesUtilities; +import org.netbeans.api.editor.settings.FontColorNames; +import org.netbeans.api.editor.settings.FontColorSettings; +import org.netbeans.api.editor.settings.SimpleValueNames; +import org.netbeans.modules.editor.lib2.EditorPreferencesDefaults; +import org.netbeans.modules.editor.lib2.view.LockedViewHierarchy; +import org.netbeans.modules.editor.lib2.view.ParagraphViewDescriptor; +import org.netbeans.modules.editor.lib2.view.ViewHierarchy; +import org.netbeans.modules.editor.lib2.view.ViewHierarchyEvent; +import org.netbeans.modules.editor.lib2.view.ViewHierarchyListener; +import org.openide.util.Lookup; +import org.openide.util.LookupEvent; +import org.openide.util.LookupListener; +import org.openide.util.NbBundle; +import org.openide.util.WeakListeners; + +/** + * Code Folding Side Bar. Component responsible for drawing folding signs and responding + * on user fold/unfold action. + * + * @author Martin Roskanin + * @deprecated You should use {@link FoldUtilities#createSidebarComponent(javax.swing.text.JTextComponent)} or + * {@link FoldUtilities#getFoldingSidebarFactory()} instead. Subclassing CodeFoldingSidebar + * is no longer actively supported, though still working. + */ +@Deprecated +public class CodeFoldingSideBar extends JComponent implements Accessible { + + private static final Logger LOG = Logger.getLogger(CodeFoldingSideBar.class.getName()); + + /** This field should be treated as final. Subclasses are forbidden to change it. + * @deprecated Without any replacement. + */ + protected Color backColor; + /** This field should be treated as final. Subclasses are forbidden to change it. + * @deprecated Without any replacement. + */ + protected Color foreColor; + /** This field should be treated as final. Subclasses are forbidden to change it. + * @deprecated Without any replacement. + */ + protected Font font; + + /** This field should be treated as final. Subclasses are forbidden to change it. */ + protected /*final*/ JTextComponent component; + private volatile AttributeSet attribs; + private Lookup.Result fcsLookupResult; + private final LookupListener fcsTracker = new LookupListener() { + public void resultChanged(LookupEvent ev) { + attribs = null; + SwingUtilities.invokeLater(new Runnable() { + public void run() { + //EMI: This is needed as maybe the DEFAULT_COLORING is changed, the font is different + // and while getMarkSize() is used in paint() and will make the artifacts bigger, + // the component itself will be the same size and it must be changed. + // See http://www.netbeans.org/issues/show_bug.cgi?id=153316 + updatePreferredSize(); + CodeFoldingSideBar.this.repaint(); + } + }); + } + }; + private final Listener listener = new Listener(); + + private boolean enabled = false; + + protected List visibleMarks = new ArrayList(); + + /** + * Mouse moved point, possibly {@code null}. Set from mouse-moved, mouse-entered + * handlers, so that painting will paint this fold in bold. -1, if mouse is not + * in the sidebar region. The value is used to compute highlighted portions of the + * folding outline. + */ + private int mousePoint = -1; + + /** + * if true, the {@link #mousePoint} has been already used to make a PaintInfo active. + * The flag is tested by {@link #traverseForward} and {@link #traverseBackward} after children + * of the current fold are processed and cleared if the {@link #mousePoint} falls to the fold area - + * fields of PaintInfo are set accordingly. + * It's also used to compute (current) mouseBoundary, so mouse movement does not trigger + * refreshes eagerly + */ + private boolean mousePointConsumed; + + /** + * Boundaries of the current area under the mouse. Can be eiher the span of the + * current fold (or part of it), or the span not occupied by any fold. Serves as an optimization + * for mouse handler, which does not trigger painting (refresh) unless mouse + * leaves this region. + */ + private Rectangle mouseBoundary; + + /** + * Y-end of the nearest fold that ends above the {@link #mousePoint}. Undefined if mousePoint is null. + * These two variables are initialized at each level of folds, and help to compute {@link #mouseBoundary} for + * the case the mousePointer is OUTSIDE all children (or outside all folds). + */ + private int lowestAboveMouse = -1; + + /** + * Y-begin of the nearest fold, which starts below the {@link #mousePoint}. Undefined if mousePoint is null + */ + private int topmostBelowMouse = Integer.MAX_VALUE; + + /** Paint operations */ + public static final int PAINT_NOOP = 0; + /** + * Normal opening +- marker + */ + public static final int PAINT_MARK = 1; + + /** + * Vertical line - typically at the end of the screen + */ + public static final int PAINT_LINE = 2; + + /** + * End angled line, without a sign + */ + public static final int PAINT_END_MARK = 3; + + /** + * Single-line marker, both start and end + */ + public static final int SINGLE_PAINT_MARK = 4; + + /** + * Marker value for {@link #mousePoint} indicating that mouse is outside the Component. + */ + private static final int NO_MOUSE_POINT = -1; + + /** + * Stroke used to draw inactive (regular) fold outlines. + */ + private static Stroke LINE_DASHED = new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, + 1f, new float[] { 1f, 1f }, 0f); + + private boolean alreadyPresent; + + /** + * Stroke used to draw outlines for 'active' fold + */ + private static final Stroke LINE_BOLD = new BasicStroke(2, BasicStroke.CAP_ROUND, BasicStroke.JOIN_MITER); + + private final Preferences prefs; + private final PreferenceChangeListener prefsListener = new PreferenceChangeListener() { + public void preferenceChange(PreferenceChangeEvent evt) { + String key = evt == null ? null : evt.getKey(); + if (key == null || SimpleValueNames.CODE_FOLDING_ENABLE.equals(key)) { + updateColors(); + + boolean newEnabled = prefs.getBoolean(SimpleValueNames.CODE_FOLDING_ENABLE, EditorPreferencesDefaults.defaultCodeFoldingEnable); + if (enabled != newEnabled) { + enabled = newEnabled; + updatePreferredSize(); + } + } + } + }; + + private void checkRepaint(ViewHierarchyEvent vhe) { + if (!vhe.isChangeY()) { + // does not obscur sidebar graphics + return; + } + + SwingUtilities.invokeLater(new Runnable() { + public void run() { + updatePreferredSize(); + CodeFoldingSideBar.this.repaint(); + } + }); + } + + /** + * @deprecated Don't use this constructor, it does nothing! + */ + public CodeFoldingSideBar() { + component = null; + prefs = null; + throw new IllegalStateException("Do not use this constructor!"); //NOI18N + } + + public CodeFoldingSideBar(JTextComponent component){ + super(); + this.component = component; + + if (component.getClientProperty("org.netbeans.editor.CodeFoldingSidebar") == null) { + component.putClientProperty("org.netbeans.editor.CodeFoldingSidebar", Boolean.TRUE); + } else { + alreadyPresent = true; + } + + addMouseListener(listener); + addMouseMotionListener(listener); + + FoldHierarchy foldHierarchy = FoldHierarchy.get(component); + foldHierarchy.addFoldHierarchyListener(WeakListeners.create(FoldHierarchyListener.class, listener, foldHierarchy)); + + Document doc = getDocument(); + doc.addDocumentListener(WeakListeners.document(listener, doc)); + setOpaque(true); + + prefs = MimeLookup.getLookup(org.netbeans.lib.editor.util.swing.DocumentUtilities.getMimeType(component)).lookup(Preferences.class); + prefs.addPreferenceChangeListener(WeakListeners.create(PreferenceChangeListener.class, prefsListener, prefs)); + prefsListener.preferenceChange(null); + + ViewHierarchy.get(component).addViewHierarchyListener(new ViewHierarchyListener() { + + @Override + public void viewHierarchyChanged(ViewHierarchyEvent evt) { + checkRepaint(evt); + } + + }); + } + + private void updatePreferredSize() { + if (enabled && !alreadyPresent) { + setPreferredSize(new Dimension(getColoring().getFont().getSize(), component.getHeight())); + setMaximumSize(new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE)); + }else{ + setPreferredSize(new Dimension(0,0)); + setMaximumSize(new Dimension(0,0)); + } + revalidate(); + } + + private void updateColors() { + Coloring c = getColoring(); + this.backColor = c.getBackColor(); + this.foreColor = c.getForeColor(); + this.font = c.getFont(); + } + + /** + * This method should be treated as final. Subclasses are forbidden to override it. + * @return The background color used for painting this component. + * @deprecated Without any replacement. + */ + protected Color getBackColor() { + if (backColor == null) { + updateColors(); + } + return backColor; + } + + /** + * This method should be treated as final. Subclasses are forbidden to override it. + * @return The foreground color used for painting this component. + * @deprecated Without any replacement. + */ + protected Color getForeColor() { + if (foreColor == null) { + updateColors(); + } + return foreColor; + } + + /** + * This method should be treated as final. Subclasses are forbidden to override it. + * @return The font used for painting this component. + * @deprecated Without any replacement. + */ + protected Font getColoringFont() { + if (font == null) { + updateColors(); + } + return font; + } + + // overriding due to issue #60304 + public @Override void update(Graphics g) { + } + + protected void collectPaintInfos( + View rootView, Fold fold, Map map, int level, int startIndex, int endIndex + ) throws BadLocationException { + //never called + } + + /** + * Adjust lowest/topmost boundaries from the Fold range y1-y2. + * @param y1 + * @param y2 + * @param level + */ + private void setMouseBoundaries(int y1, int y2, int level) { + if (!hasMousePoint() || mousePointConsumed) { + return; + } + int y = mousePoint; + if (y2 < y && lowestAboveMouse < y2) { + LOG.log(Level.FINEST, "lowestAbove at {1}: {0}", new Object[] { y2, level }); + lowestAboveMouse = y2; + } + if (y1 > y && topmostBelowMouse > y1) { + LOG.log(Level.FINEST, "topmostBelow at {1}: {0}", new Object[] { y1, level }); + topmostBelowMouse = y1; + } + } + + /* + * Even collapsed fold MAY contain a continuation line, IF one of folds on the same line is NOT collapsed. Such a fold should + * then visually span multiple lines && be marked as collapsed. + */ + + protected List getPaintInfo(Rectangle clip) throws BadLocationException { + javax.swing.plaf.TextUI textUI = component.getUI(); + if (!(textUI instanceof BaseTextUI)) { + return Collections.emptyList(); + } + BaseTextUI baseTextUI = (BaseTextUI)textUI; + BaseDocument bdoc = Utilities.getDocument(component); + if (bdoc == null) { + return Collections.emptyList(); + } + mousePointConsumed = false; + mouseBoundary = null; + topmostBelowMouse = Integer.MAX_VALUE; + lowestAboveMouse = -1; + bdoc.readLock(); + try { + int startPos = baseTextUI.getPosFromY(clip.y); + int endPos = baseTextUI.viewToModel(component, Short.MAX_VALUE / 2, clip.y + clip.height); + + if (startPos < 0 || endPos < 0) { + // editor window is not properly sized yet; return no infos + return Collections.emptyList(); + } + + // #218282: if the view hierarchy is not yet updated, the Y coordinate may map to an incorrect offset outside + // the document. + int docLen = bdoc.getLength(); + if (startPos >= docLen || endPos > docLen) { + return Collections.emptyList(); + } + + startPos = Utilities.getRowStart(bdoc, startPos); + endPos = Utilities.getRowEnd(bdoc, endPos); + + FoldHierarchy hierarchy = FoldHierarchy.get(component); + hierarchy.lock(); + try { + View rootView = Utilities.getDocumentView(component); + if (rootView != null) { + Object [] arr = getFoldList(hierarchy.getRootFold(), startPos, endPos); + @SuppressWarnings("unchecked") + List foldList = (List) arr[0]; + int idxOfFirstFoldStartingInsideClip = (Integer) arr[1]; + + /* + * Note: + * + * The Map is keyed by Y-VISUAL position of the fold mark, not the textual offset of line start. + * This is because several folds may occupy the same line, while only one + sign is displayed, + * and affect the last fold in the row. + */ + NavigableMap map = new TreeMap(); + // search backwards + for(int i = idxOfFirstFoldStartingInsideClip - 1; i >= 0; i--) { + Fold fold = foldList.get(i); + if (!traverseBackwards(fold, bdoc, baseTextUI, startPos, endPos, 0, map)) { + break; + } + } + + // search forward + for(int i = idxOfFirstFoldStartingInsideClip; i < foldList.size(); i++) { + Fold fold = foldList.get(i); + if (!traverseForward(fold, bdoc, baseTextUI, startPos, endPos, 0, map)) { + break; + } + } + + if (map.isEmpty() && foldList.size() > 0) { + assert foldList.size() == 1; + PaintInfo pi = new PaintInfo(PAINT_LINE, 0, clip.y, clip.height, -1, -1); + mouseBoundary = new Rectangle(0, 0, 0, clip.height); + LOG.log(Level.FINEST, "Mouse boundary for full side line set to: {0}", mouseBoundary); + if (hasMousePoint()) { + pi.markActive(true, true, true); + } + return Collections.singletonList(pi); + } else { + if (mouseBoundary == null) { + mouseBoundary = makeMouseBoundary(clip.y, clip.y + clip.height); + LOG.log(Level.FINEST, "Mouse boundary not set, defaulting to: {0}", mouseBoundary); + } + return new ArrayList(map.values()); + } + } else { + return Collections.emptyList(); + } + } finally { + hierarchy.unlock(); + } + } finally { + bdoc.readUnlock(); + } + } + + /** + * Adds a paint info to the map. If a paintinfo already exists, it merges + * the structures, so the painting process can just follow the instructions. + * + * @param infos + * @param yOffset + * @param nextInfo + */ + private void addPaintInfo(Map infos, int yOffset, PaintInfo nextInfo) { + PaintInfo prevInfo = infos.get(yOffset); + nextInfo.mergeWith(prevInfo); + infos.put(yOffset, nextInfo); + } + + private boolean traverseForward(Fold f, BaseDocument doc, BaseTextUI btui, int lowerBoundary, int upperBoundary,int level, NavigableMap infos) throws BadLocationException { +// System.out.println("~~~ traverseForward<" + lowerBoundary + ", " + upperBoundary +// + ">: fold=<" + f.getStartOffset() + ", " + f.getEndOffset() + "> " +// + (f.getStartOffset() > upperBoundary ? ", f.gSO > uB" : "") +// + ", level=" + level); + + if (f.getStartOffset() > upperBoundary) { + return false; + } + + int lineStartOffset1 = Utilities.getRowStart(doc, f.getStartOffset()); + int lineStartOffset2 = Utilities.getRowStart(doc, f.getEndOffset()); + int y1 = btui.getYFromPos(lineStartOffset1); + int h = btui.getEditorUI().getLineHeight(); + int y2 = btui.getYFromPos(lineStartOffset2); + + // the 'active' flags can be set only after children are processed; highlights + // correspond to the innermost expanded child. + boolean activeMark = false; + boolean activeIn = false; + boolean activeOut = false; + PaintInfo spi; + boolean activated; + + if (y1 == y2) { + // whole fold is on a single line + spi = new PaintInfo(SINGLE_PAINT_MARK, level, y1, h, f.isCollapsed(), lineStartOffset1, lineStartOffset2); + if (activated = isActivated(y1, y1 + h)) { + activeMark = true; + } + addPaintInfo(infos, y1, spi); + } else { + // fold spans multiple lines + spi = new PaintInfo(PAINT_MARK, level, y1, h, f.isCollapsed(), lineStartOffset1, lineStartOffset2); + if (activated = isActivated(y1, y2 + h / 2)) { + activeMark = true; + activeOut = true; + } + addPaintInfo(infos, y1, spi); + } + + setMouseBoundaries(y1, y2 + h / 2, level); + + // Handle end mark after possible inner folds were processed because + // otherwise if there would be two nested folds both ending at the same line + // then the end mark for outer one would be replaced by an end mark for inner one + // (same key in infos map) and the painting code would continue to paint line marking a fold + // until next fold is reached (or end of doc). + PaintInfo epi = null; + if (y1 != y2 && !f.isCollapsed() && f.getEndOffset() <= upperBoundary) { + epi = new PaintInfo(PAINT_END_MARK, level, y2, h, lineStartOffset1, lineStartOffset2); + addPaintInfo(infos, y2, epi); + } + + // save the topmost/lowest information, reset for child processing + int topmost = topmostBelowMouse; + int lowest = lowestAboveMouse; + topmostBelowMouse = y2 + h / 2; + lowestAboveMouse = y1; + + try { + if (!f.isCollapsed()) { + Object [] arr = getFoldList(f, lowerBoundary, upperBoundary); + @SuppressWarnings("unchecked") + List foldList = (List) arr[0]; + int idxOfFirstFoldStartingInsideClip = (Integer) arr[1]; + + // search backwards + for(int i = idxOfFirstFoldStartingInsideClip - 1; i >= 0; i--) { + Fold fold = foldList.get(i); + if (!traverseBackwards(fold, doc, btui, lowerBoundary, upperBoundary, level + 1, infos)) { + break; + } + } + + // search forward + for(int i = idxOfFirstFoldStartingInsideClip; i < foldList.size(); i++) { + Fold fold = foldList.get(i); + if (!traverseForward(fold, doc, btui, lowerBoundary, upperBoundary, level + 1, infos)) { + return false; + } + } + } + if (!mousePointConsumed && activated) { + mousePointConsumed = true; + mouseBoundary = makeMouseBoundary(y1, y2 + h); + LOG.log(Level.FINEST, "Mouse boundary set to: {0}", mouseBoundary); + spi.markActive(activeMark, activeIn, activeOut); + if (epi != null) { + epi.markActive(true, true, false); + } + markDeepChildrenActive(infos, y1, y2, level); + } + } finally { + topmostBelowMouse = topmost; + lowestAboveMouse = lowest; + } + return true; + } + + /** + * Sets outlines of all children to 'active'. Assuming yFrom and yTo are from-to Y-coordinates of the parent + * fold, it finds all nested folds (folds, which are in between yFrom and yTo) and changes their in/out lines + * as active. + * The method returns Y start coordinate of the 1st child found. + * + * @param infos fold infos collected so far + * @param yFrom upper Y-coordinate of the parent fold + * @param yTo lower Y-coordinate of the parent fold + * @param level level of the parent fold + * @return Y-coordinate of the 1st child. + */ + private int markDeepChildrenActive(NavigableMap infos, int yFrom, int yTo, int level) { + int result = Integer.MAX_VALUE; + Map m = infos.subMap(yFrom, yTo); + for (Map.Entry me : m.entrySet()) { + PaintInfo pi = me.getValue(); + int y = pi.getPaintY(); + if (y > yFrom && y < yTo) { + if (LOG.isLoggable(Level.FINEST)) { + LOG.log(Level.FINEST, "Marking chind as active: {0}", pi); + } + pi.markActive(false, true, true); + if (y < result) { + y = result; + } + } + } + return result; + } + + /** + * Returns stroke appropriate for painting (in)active outlines + * @param s the default stroke + * @param active true for active outlines + * @return value of 's' or a Stroke which should be used to paint the outline. + */ + private static Stroke getStroke(Stroke s, boolean active) { + if (active) { + return LINE_BOLD; + } else { + return s; + } + } + + private boolean traverseBackwards(Fold f, BaseDocument doc, BaseTextUI btui, int lowerBoundary, int upperBoundary, int level, NavigableMap infos) throws BadLocationException { +// System.out.println("~~~ traverseBackwards<" + lowerBoundary + ", " + upperBoundary +// + ">: fold=<" + f.getStartOffset() + ", " + f.getEndOffset() + "> " +// + (f.getEndOffset() < lowerBoundary ? ", f.gEO < lB" : "") +// + ", level=" + level); + + if (f.getEndOffset() < lowerBoundary) { + return false; + } + + int lineStartOffset1 = Utilities.getRowStart(doc, f.getStartOffset()); + int lineStartOffset2 = Utilities.getRowStart(doc, f.getEndOffset()); + int h = btui.getEditorUI().getLineHeight(); + + boolean activeMark = false; + boolean activeIn = false; + boolean activeOut = false; + PaintInfo spi = null; + PaintInfo epi = null; + boolean activated = false; + int y1 = 0; + int y2 = 0; + + if (lineStartOffset1 == lineStartOffset2) { + // whole fold is on a single line + y2 = y1 = btui.getYFromPos(lineStartOffset1); + spi = new PaintInfo(SINGLE_PAINT_MARK, level, y1, h, f.isCollapsed(), lineStartOffset1, lineStartOffset1); + if (activated = isActivated(y1, y1 + h)) { + activeMark = true; + } + addPaintInfo(infos, y1, spi); + } else { + y2 = btui.getYFromPos(lineStartOffset2); + // fold spans multiple lines + y1 = btui.getYFromPos(lineStartOffset1); + activated = isActivated(y1, y2 + h / 2); + if (f.getStartOffset() >= upperBoundary) { + spi = new PaintInfo(PAINT_MARK, level, y1, h, f.isCollapsed(), lineStartOffset1, lineStartOffset2); + if (activated) { + activeMark = true; + activeOut = true; + } + addPaintInfo(infos, y1, spi); + } + + if (!f.isCollapsed() && f.getEndOffset() <= upperBoundary) { + activated |= isActivated(y1, y2 + h / 2); + epi = new PaintInfo(PAINT_END_MARK, level, y2, h, lineStartOffset1, lineStartOffset2); + addPaintInfo(infos, y2, epi); + } + } + + setMouseBoundaries(y1, y2 + h / 2, level); + + // save the topmost/lowest information, reset for child processing + int topmost = topmostBelowMouse; + int lowest = lowestAboveMouse; + topmostBelowMouse = y2 + h /2; + lowestAboveMouse = y1; + + try { + if (!f.isCollapsed()) { + Object [] arr = getFoldList(f, lowerBoundary, upperBoundary); + @SuppressWarnings("unchecked") + List foldList = (List) arr[0]; + int idxOfFirstFoldStartingInsideClip = (Integer) arr[1]; + + // search backwards + for(int i = idxOfFirstFoldStartingInsideClip - 1; i >= 0; i--) { + Fold fold = foldList.get(i); + if (!traverseBackwards(fold, doc, btui, lowerBoundary, upperBoundary, level + 1, infos)) { + return false; + } + } + + // search forward + for(int i = idxOfFirstFoldStartingInsideClip; i < foldList.size(); i++) { + Fold fold = foldList.get(i); + if (!traverseForward(fold, doc, btui, lowerBoundary, upperBoundary, level + 1, infos)) { + break; + } + } + } + if (!mousePointConsumed && activated) { + mousePointConsumed = true; + mouseBoundary = makeMouseBoundary(y1, y2 + h); + LOG.log(Level.FINEST, "Mouse boundary set to: {0}", mouseBoundary); + if (spi != null) { + spi.markActive(activeMark, activeIn, activeOut); + } + if (epi != null) { + epi.markActive(true, true, false); + } + int lowestChild = markDeepChildrenActive(infos, y1, y2, level); + if (lowestChild < Integer.MAX_VALUE && lineStartOffset1 < upperBoundary) { + // the fold starts above the screen clip region, and is 'activated'. We need to setup instructions to draw activated line up to the + // 1st child marker. + epi = new PaintInfo(PAINT_LINE, level, y1, y2 - y1, false, lineStartOffset1, lineStartOffset2); + epi.markActive(true, true, false); + addPaintInfo(infos, y1, epi); + } + } + } finally { + topmostBelowMouse = topmost; + lowestAboveMouse = lowest; + } + return true; + } + + private Rectangle makeMouseBoundary(int y1, int y2) { + if (!hasMousePoint()) { + return null; + } + if (topmostBelowMouse < Integer.MAX_VALUE) { + y2 = topmostBelowMouse; + } + if (lowestAboveMouse > -1) { + y1 = lowestAboveMouse; + } + return new Rectangle(0, y1, 0, y2 - y1); + } + + protected EditorUI getEditorUI(){ + return Utilities.getEditorUI(component); + } + + protected Document getDocument(){ + return component.getDocument(); + } + + + private Fold getLastLineFold(FoldHierarchy hierarchy, int rowStart, int rowEnd, boolean shift){ + Fold fold = FoldUtilities.findNearestFold(hierarchy, rowStart); + Fold prevFold = fold; + while (fold != null && fold.getStartOffset()= startY && mouseY <= nextY) { + LOG.log(Level.FINEST, "Starting line clicked, ignoring. MouseY={0}, startY={1}, nextY={2}", + new Object[] { mouseY, startY, nextY }); + return; + } + + startY = textUI.getYFromPos(endOffset); + nextLineOffset = Utilities.getRowStart(bdoc, endOffset, 1); + nextY = textUI.getYFromPos(nextLineOffset); + + if (mouseY >= startY && mouseY <= nextY) { + // the mouse can be positioned above the marker (the fold found above), or + // below it; in that case, the immediate enclosing fold should be used - should be the fold + // that corresponds to the nextLineOffset, if any + int h2 = (startY + nextY) / 2; + if (mouseY >= h2) { + Fold f2 = f; + + f = FoldUtilities.findOffsetFold(hierarchy, nextLineOffset); + if (f == null) { + // fold does not exist for the position below end-of-fold indicator + return; + } + } + + } + + LOG.log(Level.FINEST, "Collapsing fold: {0}", f); + hierarchy.collapse(f); + } finally { + hierarchy.unlock(); + } + } finally { + bdoc.readUnlock(); + } + } + + private void performAction(final Mark mark, final boolean shiftFold) { + Document doc = component.getDocument(); + doc.render(new Runnable() { + @Override + public void run() { + ViewHierarchy vh = ViewHierarchy.get(component); + LockedViewHierarchy lockedVH = vh.lock(); + try { + int pViewIndex = lockedVH.yToParagraphViewIndex(mark.y + mark.size / 2); + if (pViewIndex >= 0) { + ParagraphViewDescriptor pViewDesc = lockedVH.getParagraphViewDescriptor(pViewIndex); + int pViewStartOffset = pViewDesc.getStartOffset(); + int pViewEndOffset = pViewStartOffset + pViewDesc.getLength(); + // Find corresponding fold + FoldHierarchy foldHierarchy = FoldHierarchy.get(component); + foldHierarchy.lock(); + try { + int rowStart = javax.swing.text.Utilities.getRowStart(component, pViewStartOffset); + int rowEnd = javax.swing.text.Utilities.getRowEnd(component, pViewStartOffset); + Fold clickedFold = getLastLineFold(foldHierarchy, rowStart, rowEnd, shiftFold);//FoldUtilities.findNearestFold(foldHierarchy, viewStartOffset); + if (clickedFold != null && clickedFold.getStartOffset() < pViewEndOffset) { + foldHierarchy.toggle(clickedFold); + } + } catch (BadLocationException ble) { + LOG.log(Level.WARNING, null, ble); + } finally { + foldHierarchy.unlock(); + } + } + } finally { + lockedVH.unlock(); + } + } + }); + } + + protected int getMarkSize(Graphics g){ + if (g != null){ + FontMetrics fm = g.getFontMetrics(getColoring().getFont()); + if (fm != null){ + int ret = fm.getAscent() - fm.getDescent(); + return ret - ret%2; + } + } + return -1; + } + + private boolean hasMousePoint() { + return mousePoint >= 0; + } + + private boolean isActivated(int y1, int y2) { + return hasMousePoint() && + (mousePoint >= y1 && mousePoint < y2); + } + + private void drawFoldLine(Graphics2D g2d, boolean active, int x1, int y1, int x2, int y2) { + Stroke origStroke = g2d.getStroke(); + g2d.setStroke(getStroke(origStroke, active)); + g2d.drawLine(x1, y1, x2, y2); + g2d.setStroke(origStroke); + } + + protected @Override void paintComponent(Graphics g) { + if (!enabled) { + return; + } + + Rectangle clip = getVisibleRect();//g.getClipBounds(); + visibleMarks.clear(); + + Coloring coloring = getColoring(); + g.setColor(coloring.getBackColor()); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + g.setColor(coloring.getForeColor()); + + AbstractDocument adoc = (AbstractDocument)component.getDocument(); + adoc.readLock(); + try { + List ps = getPaintInfo(clip); + Font defFont = coloring.getFont(); + int markSize = getMarkSize(g); + int halfMarkSize = markSize / 2; + int markX = (defFont.getSize() - markSize) / 2; // x position of mark rectangle + int plusGap = (int)Math.round(markSize / 3.8); // distance between mark rectangle vertical side and start/end of minus sign + int lineX = markX + halfMarkSize; // x position of the centre of mark + + LOG.fine("CFSBar: PAINT START ------\n"); + int descent = g.getFontMetrics(defFont).getDescent(); + PaintInfo previousInfo = null; + Graphics2D g2d = (Graphics2D)g; + LOG.log(Level.FINEST, "MousePoint: {0}", mousePoint); + + for(PaintInfo paintInfo : ps) { + boolean isFolded = paintInfo.isCollapsed(); + int y = paintInfo.getPaintY(); + int height = paintInfo.getPaintHeight(); + int markY = y + descent; // y position of mark rectangle + int paintOperation = paintInfo.getPaintOperation(); + + if (previousInfo == null) { + if (paintInfo.hasLineIn()) { + if (LOG.isLoggable(Level.FINE)) { + LOG.fine("prevInfo=NULL; y=" + y + ", PI:" + paintInfo + "\n"); // NOI18N + } + drawFoldLine(g2d, paintInfo.lineInActive, lineX, clip.y, lineX, y); + } + } else { + if (previousInfo.hasLineOut() || paintInfo.hasLineIn()) { + // Draw middle vertical line + int prevY = previousInfo.getPaintY(); + if (LOG.isLoggable(Level.FINE)) { + LOG.log(Level.FINE, "prevInfo={0}; y=" + y + ", PI:" + paintInfo + "\n", previousInfo); // NOI18N + } + drawFoldLine(g2d, previousInfo.lineOutActive || paintInfo.lineInActive, lineX, prevY + previousInfo.getPaintHeight(), lineX, y); + } + } + + if (paintInfo.hasSign()) { + g.drawRect(markX, markY, markSize, markSize); + g.drawLine(plusGap + markX, markY + halfMarkSize, markSize + markX - plusGap, markY + halfMarkSize); + String opStr = (paintOperation == PAINT_MARK) ? "PAINT_MARK" : "SINGLE_PAINT_MARK"; // NOI18N + if (isFolded) { + if (LOG.isLoggable(Level.FINE)) { + LOG.fine(opStr + ": folded; y=" + y + ", PI:" + paintInfo + "\n"); // NOI18N + } + g.drawLine(lineX, markY + plusGap, lineX, markY + markSize - plusGap); + } + if (paintOperation != SINGLE_PAINT_MARK) { + if (LOG.isLoggable(Level.FINE)) { + LOG.fine(opStr + ": non-single; y=" + y + ", PI:" + paintInfo + "\n"); // NOI18N + } + } + if (paintInfo.hasLineIn()) { //[PENDING] + drawFoldLine(g2d, paintInfo.lineInActive, lineX, y, lineX, markY); + } + if (paintInfo.hasLineOut()) { + // This is an error in case there's a next paint info at the same y which is an end mark + // for this mark (it must be cleared explicitly). + drawFoldLine(g2d, paintInfo.lineOutActive, lineX, markY + markSize, lineX, y + height); + } + visibleMarks.add(new Mark(markX, markY, markSize, isFolded)); + + } else if (paintOperation == PAINT_LINE) { + if (LOG.isLoggable(Level.FINE)) { + LOG.fine("PAINT_LINE: y=" + y + ", PI:" + paintInfo + "\n"); // NOI18N + } + // FIXME !! + drawFoldLine(g2d, paintInfo.signActive, lineX, y, lineX, y + height ); + } else if (paintOperation == PAINT_END_MARK) { + if (LOG.isLoggable(Level.FINE)) { + LOG.fine("PAINT_END_MARK: y=" + y + ", PI:" + paintInfo + "\n"); // NOI18N + } + if (previousInfo == null || y != previousInfo.getPaintY()) { + drawFoldLine(g2d, paintInfo.lineInActive, lineX, y, lineX, y + height / 2); + drawFoldLine(g2d, paintInfo.signActive, lineX, y + height / 2, lineX + halfMarkSize, y + height / 2); + if (paintInfo.getInnerLevel() > 0) {//[PENDING] + if (LOG.isLoggable(Level.FINE)) { + LOG.fine(" PAINT middle-line\n"); // NOI18N + } + drawFoldLine(g2d, paintInfo.lineOutActive, lineX, y + height / 2, lineX, y + height); + } + } + } + + previousInfo = paintInfo; + } + + if (previousInfo != null && + (previousInfo.getInnerLevel() > 0 || + (previousInfo.getPaintOperation() == PAINT_MARK && !previousInfo.isCollapsed())) + ) { + drawFoldLine(g2d, previousInfo.lineOutActive, + lineX, previousInfo.getPaintY() + previousInfo.getPaintHeight(), lineX, clip.y + clip.height); + } + + } catch (BadLocationException ble) { + LOG.log(Level.WARNING, null, ble); + } finally { + LOG.fine("CFSBar: PAINT END ------\n\n"); + adoc.readUnlock(); + } + } + + private static Object [] getFoldList(Fold parentFold, int start, int end) { + List ret = new ArrayList(); + + int index = FoldUtilities.findFoldEndIndex(parentFold, start); + int foldCount = parentFold.getFoldCount(); + int idxOfFirstFoldStartingInside = -1; + while (index < foldCount) { + Fold f = parentFold.getFold(index); + if (f.getStartOffset() <= end) { + ret.add(f); + } else { + break; // no more relevant folds + } + if (idxOfFirstFoldStartingInside == -1 && f.getStartOffset() >= start) { + idxOfFirstFoldStartingInside = ret.size() - 1; + } + index++; + } + + return new Object [] { ret, idxOfFirstFoldStartingInside != -1 ? idxOfFirstFoldStartingInside : ret.size() }; + } + + /** + * This class should be never used by other code; will be made private + */ + public class PaintInfo { + + int paintOperation; + /** + * level of the 1st marker on the line + */ + int innerLevel; + + /** + * Y-coordinate of the cell + */ + int paintY; + + /** + * Height of the paint cell + */ + int paintHeight; + + /** + * State of the marker (+/-) + */ + boolean isCollapsed; + + /** + * all markers on the line are collapsed + */ + boolean allCollapsed; + int startOffset; + int endOffset; + /** + * nesting level of the last marker on the line + */ + int outgoingLevel; + + /** + * Force incoming line (from above) to be present + */ + boolean lineIn; + + /** + * Force outgoing line (down from marker) to be present + */ + boolean lineOut; + + /** + * The 'incoming' (upper) line should be painted as active + */ + boolean lineInActive; + + /** + * The 'outgoing' (down) line should be painted as active + */ + boolean lineOutActive; + + /** + * The sign/marker itself should be painted as active + */ + boolean signActive; + + public PaintInfo(int paintOperation, int innerLevel, int paintY, int paintHeight, boolean isCollapsed, int startOffset, int endOffset){ + this.paintOperation = paintOperation; + this.innerLevel = this.outgoingLevel = innerLevel; + this.paintY = paintY; + this.paintHeight = paintHeight; + this.isCollapsed = this.allCollapsed = isCollapsed; + this.startOffset = startOffset; + this.endOffset = endOffset; + + switch (paintOperation) { + case PAINT_MARK: + lineIn = false; + lineOut = true; + outgoingLevel++; + break; + case SINGLE_PAINT_MARK: + lineIn = false; + lineOut = false; + break; + case PAINT_END_MARK: + lineIn = true; + lineOut = false; + isCollapsed = true; + allCollapsed = true; + break; + case PAINT_LINE: + lineIn = lineOut = true; + break; + } + } + + /** + * Sets active flags on inidivual parts of the mark + * @param mark + * @param lineIn + * @param lineOut S + */ + void markActive(boolean mark, boolean lineIn, boolean lineOut) { + this.signActive |= mark; + this.lineInActive |= lineIn; + this.lineOutActive |= lineOut; + } + + boolean hasLineIn() { + return lineIn || innerLevel > 0; + } + + boolean hasLineOut() { + return lineOut || outgoingLevel > 0 || (paintOperation != SINGLE_PAINT_MARK && !isAllCollapsed()); + } + + public PaintInfo(int paintOperation, int innerLevel, int paintY, int paintHeight, int startOffset, int endOffset){ + this(paintOperation, innerLevel, paintY, paintHeight, false, startOffset, endOffset); + } + + public int getPaintOperation(){ + return paintOperation; + } + + public int getInnerLevel(){ + return innerLevel; + } + + public int getPaintY(){ + return paintY; + } + + public int getPaintHeight(){ + return paintHeight; + } + + public boolean isCollapsed(){ + return isCollapsed; + } + + boolean isAllCollapsed() { + return allCollapsed; + } + + public void setPaintOperation(int paintOperation){ + this.paintOperation = paintOperation; + } + + public void setInnerLevel(int innerLevel){ + this.innerLevel = innerLevel; + } + + public @Override String toString(){ + StringBuffer sb = new StringBuffer(""); + if (paintOperation == PAINT_MARK){ + sb.append("PAINT_MARK"); // NOI18N + }else if (paintOperation == PAINT_LINE){ + sb.append("PAINT_LINE"); // NOI18N + }else if (paintOperation == PAINT_END_MARK) { + sb.append("PAINT_END_MARK"); // NOI18N + }else if (paintOperation == SINGLE_PAINT_MARK) { + sb.append("SINGLE_PAINT_MARK"); + } + sb.append(",L:").append(innerLevel).append("/").append(outgoingLevel); // NOI18N + sb.append(',').append(isCollapsed ? "C" : "E"); // NOI18N + sb.append(", start=").append(startOffset).append(", end=").append(endOffset); + sb.append(", lineIn=").append(lineIn).append(", lineOut=").append(lineOut); + return sb.toString(); + } + + boolean hasSign() { + return paintOperation == PAINT_MARK || paintOperation == SINGLE_PAINT_MARK; + } + + + void mergeWith(PaintInfo prevInfo) { + if (prevInfo == null) { + return; + } + + int operation = this.paintOperation; + boolean lineIn = prevInfo.lineIn; + boolean lineOut = prevInfo.lineOut; + + LOG.log(Level.FINE, "Merging {0} with {1}: ", new Object[] { this, prevInfo }); + if (prevInfo.getPaintOperation() == PAINT_END_MARK) { + // merge with start|single -> start mark + line-in + lineIn = true; + } else { + operation = PAINT_MARK; + } + + int level1 = Math.min(prevInfo.innerLevel, innerLevel); + int level2 = prevInfo.outgoingLevel; + + if (getPaintOperation() == PAINT_END_MARK + && innerLevel == prevInfo.outgoingLevel) { + // if merging end marker at the last level, update to the new outgoing level + level2 = outgoingLevel; + } else if (!isCollapsed) { + level2 = Math.max(prevInfo.outgoingLevel, outgoingLevel); + } + + if (prevInfo.getInnerLevel() < getInnerLevel()) { + int paintFrom = Math.min(prevInfo.paintY, paintY); + int paintTo = Math.max(prevInfo.paintY + prevInfo.paintHeight, paintY + paintHeight); + // at least one collapsed -> paint plus sign + boolean collapsed = prevInfo.isCollapsed() || isCollapsed(); + int offsetFrom = Math.min(prevInfo.startOffset, startOffset); + int offsetTo = Math.max(prevInfo.endOffset, endOffset); + + this.paintY = paintFrom; + this.paintHeight = paintTo - paintFrom; + this.isCollapsed = collapsed; + this.startOffset = offsetFrom; + this.endOffset = offsetTo; + } + this.paintOperation = operation; + this.allCollapsed = prevInfo.allCollapsed && allCollapsed; + this.innerLevel = level1; + this.outgoingLevel = level2; + this.lineIn |= lineIn; + this.lineOut |= lineOut; + + this.signActive |= prevInfo.signActive; + this.lineInActive |= prevInfo.lineInActive; + this.lineOutActive |= prevInfo.lineOutActive; + + LOG.log(Level.FINE, "Merged result: {0}", this); + } + } + + /** Keeps info of visible folding mark */ + public class Mark{ + public int x; + public int y; + public int size; + public boolean isFolded; + + public Mark(int x, int y, int size, boolean isFolded){ + this.x = x; + this.y = y; + this.size = size; + this.isFolded = isFolded; + } + } + + private final class Listener extends MouseAdapter implements FoldHierarchyListener, DocumentListener, Runnable { + + public Listener(){ + } + + // -------------------------------------------------------------------- + // FoldHierarchyListener implementation + // -------------------------------------------------------------------- + + public void foldHierarchyChanged(FoldHierarchyEvent evt) { + refresh(); + } + + // -------------------------------------------------------------------- + // DocumentListener implementation + // -------------------------------------------------------------------- + + public void insertUpdate(DocumentEvent evt) { + if (!(evt instanceof BaseDocumentEvent)) return; + + BaseDocumentEvent bevt = (BaseDocumentEvent)evt; + if (bevt.getLFCount() > 0) { // one or more lines inserted + refresh(); + } + } + + public void removeUpdate(DocumentEvent evt) { + if (!(evt instanceof BaseDocumentEvent)) return; + + BaseDocumentEvent bevt = (BaseDocumentEvent)evt; + if (bevt.getLFCount() > 0) { // one or more lines removed + refresh(); + } + } + + public void changedUpdate(DocumentEvent evt) { + } + + // -------------------------------------------------------------------- + // MouseListener implementation + // -------------------------------------------------------------------- + + @Override + public void mousePressed (MouseEvent e) { + Mark mark = getClickedMark(e); + if (mark!=null){ + e.consume(); + performAction(mark, (e.getModifiersEx() & MouseEvent.CTRL_DOWN_MASK) > 0); + } + } + + @Override + public void mouseClicked(MouseEvent e) { + // #102288 - missing event consuming caused quick doubleclicks to break + // fold expanding/collapsing and move caret to the particular line + if (e.getClickCount() > 1) { + LOG.log(Level.FINEST, "Mouse {0}click at {1}", new Object[] { e.getClickCount(), e.getY()}); + Mark mark = getClickedMark(e); + try { + performActionAt(mark, e.getY()); + } catch (BadLocationException ex) { + LOG.log(Level.WARNING, "Error during fold expansion using sideline", ex); + } + } else { + e.consume(); + } + } + + private void refreshIfMouseOutside(Point pt) { + mousePoint = (int)pt.getY(); + if (LOG.isLoggable(Level.FINEST)) { + if (mouseBoundary == null) { + LOG.log(Level.FINEST, "Mouse boundary not set, refreshing: {0}", mousePoint); + } else { + LOG.log(Level.FINEST, "Mouse {0} inside known mouse boundary: {1}-{2}", + new Object[] { mousePoint, mouseBoundary.y, mouseBoundary.getMaxY() }); + } + } + if (mouseBoundary == null || mousePoint < mouseBoundary.y || mousePoint > mouseBoundary.getMaxY()) { + refresh(); + } + } + + @Override + public void mouseMoved(MouseEvent e) { + refreshIfMouseOutside(e.getPoint()); + } + + public void mouseEntered(MouseEvent e) { + refreshIfMouseOutside(e.getPoint()); + } + + public void mouseExited(MouseEvent e) { + mousePoint = NO_MOUSE_POINT; + refresh(); + } + + + // -------------------------------------------------------------------- + // private implementation + // -------------------------------------------------------------------- + + private Mark getClickedMark(MouseEvent e){ + if (e == null || !SwingUtilities.isLeftMouseButton(e)) { + return null; + } + + int x = e.getX(); + int y = e.getY(); + for (Mark mark : visibleMarks) { + if (x >= mark.x && x <= (mark.x + mark.size) && y >= mark.y && y <= (mark.y + mark.size)) { + return mark; + } + } + return null; + } + + private void refresh() { + SwingUtilities.invokeLater(this); + } + + @Override + public void run() { + repaint(); + } + } // End of Listener class + + @Override + public AccessibleContext getAccessibleContext() { + if (accessibleContext == null) { + accessibleContext = new AccessibleJComponent() { + public @Override AccessibleRole getAccessibleRole() { + return AccessibleRole.PANEL; + } + }; + accessibleContext.setAccessibleName(NbBundle.getMessage(CodeFoldingSideBar.class, "ACSN_CodeFoldingSideBar")); //NOI18N + accessibleContext.setAccessibleDescription(NbBundle.getMessage(CodeFoldingSideBar.class, "ACSD_CodeFoldingSideBar")); //NOI18N + } + return accessibleContext; + } + + private Coloring getColoring() { + if (attribs == null) { + if (fcsLookupResult == null) { + fcsLookupResult = MimeLookup.getLookup(org.netbeans.lib.editor.util.swing.DocumentUtilities.getMimeType(component)) + .lookupResult(FontColorSettings.class); + fcsLookupResult.addLookupListener(WeakListeners.create(LookupListener.class, fcsTracker, fcsLookupResult)); + } + + FontColorSettings fcs = fcsLookupResult.allInstances().iterator().next(); + AttributeSet attr = fcs.getFontColors(FontColorNames.CODE_FOLDING_BAR_COLORING); + if (attr == null) { + attr = fcs.getFontColors(FontColorNames.DEFAULT_COLORING); + } else { + attr = AttributesUtilities.createComposite(attr, fcs.getFontColors(FontColorNames.DEFAULT_COLORING)); + } + attribs = attr; + } + return Coloring.fromAttributeSet(attribs); + } + +} diff --git a/editor.fold/src/org/netbeans/editor/CustomFoldManager.java b/editor.fold/src/org/netbeans/editor/CustomFoldManager.java new file mode 100644 --- /dev/null +++ b/editor.fold/src/org/netbeans/editor/CustomFoldManager.java @@ -0,0 +1,769 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 1997-2010 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * Contributor(s): + * + * The Original Software is NetBeans. The Initial Developer of the Original + * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun + * Microsystems, Inc. All Rights Reserved. + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + */ + +package org.netbeans.editor; + +import org.netbeans.modules.editor.fold.*; +import javax.swing.text.Document; +import javax.swing.text.BadLocationException; +import javax.swing.text.Position; +import javax.swing.event.DocumentEvent; +import java.util.*; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.regex.Pattern; +import java.util.regex.Matcher; +import org.netbeans.api.editor.fold.Fold; +import org.netbeans.api.editor.fold.FoldHierarchy; +import org.netbeans.api.editor.fold.FoldType; +import org.netbeans.api.lexer.Token; +import org.netbeans.api.lexer.TokenHierarchy; +import org.netbeans.api.lexer.TokenSequence; +import org.netbeans.spi.editor.fold.FoldHierarchyTransaction; +import org.netbeans.spi.editor.fold.FoldManager; +import org.netbeans.spi.editor.fold.FoldManagerFactory; +import org.netbeans.spi.editor.fold.FoldOperation; +import org.openide.util.Parameters; +import org.openide.util.RequestProcessor; + +/** + * Fold maintainer that creates and updates custom folds. + * + * @author Dusan Balek, Miloslav Metelka + * @version 1.00 + * @deprecated Please use {@link org.netbeans.api.editor.fold.FoldingSupport#userFoldManager} to create an instance of + * the fold manager, or {@link org.netbeans.api.editor.fold.FoldingSupport#userFoldManagerFactory} to register factory + * instance in a layer. + */ +@Deprecated +public final class CustomFoldManager implements FoldManager, Runnable { + + private static final Logger LOG = Logger.getLogger(CustomFoldManager.class.getName()); + + public static final FoldType CUSTOM_FOLD_TYPE = new FoldType("custom-fold"); // NOI18N + + private FoldOperation operation; + private Document doc; + private org.netbeans.editor.GapObjectArray markArray = new org.netbeans.editor.GapObjectArray(); + private int minUpdateMarkOffset = Integer.MAX_VALUE; + private int maxUpdateMarkOffset = -1; + private List removedFoldList; + private HashMap customFoldId = new HashMap(); + + private static final RequestProcessor RP = new RequestProcessor(CustomFoldManager.class.getName(), + 1, false, false); + private final RequestProcessor.Task task = RP.create(this); + + private final String tokenId; + + public CustomFoldManager() { + this.tokenId = "comment"; + } + + public void init(FoldOperation operation) { + this.operation = operation; + if (LOG.isLoggable(Level.FINE)) { + LOG.log(Level.FINE, "Initialized: {0}", System.identityHashCode(this)); + } + } + + private FoldOperation getOperation() { + return operation; + } + + public void initFolds(FoldHierarchyTransaction transaction) { + doc = getOperation().getHierarchy().getComponent().getDocument(); + task.schedule(300); + } + + public void insertUpdate(DocumentEvent evt, FoldHierarchyTransaction transaction) { + processRemovedFolds(transaction); + task.schedule(300); + } + + public void removeUpdate(DocumentEvent evt, FoldHierarchyTransaction transaction) { + processRemovedFolds(transaction); + removeAffectedMarks(evt, transaction); + task.schedule(300); + } + + public void changedUpdate(DocumentEvent evt, FoldHierarchyTransaction transaction) { + } + + public void removeEmptyNotify(Fold emptyFold) { + removeFoldNotify(emptyFold); + } + + public void removeDamagedNotify(Fold damagedFold) { + removeFoldNotify(damagedFold); + } + + public void expandNotify(Fold expandedFold) { + + } + + public void release() { + if (LOG.isLoggable(Level.FINE)) { + LOG.log(Level.FINE, "Released: {0}", System.identityHashCode(this)); + } + } + + public void run() { + if (operation.isReleased()) { + if (LOG.isLoggable(Level.FINE)) { + LOG.log(Level.FINE, "Update skipped, already relaesed: {0}", System.identityHashCode(this)); + } + return; + } + ((BaseDocument) doc).readLock(); + try { + TokenHierarchy th = TokenHierarchy.get(doc); + if (th != null && th.isActive()) { + FoldHierarchy hierarchy = getOperation().getHierarchy(); + hierarchy.lock(); + try { + if (operation.isReleased()) { + if (LOG.isLoggable(Level.FINE)) { + LOG.log(Level.FINE, "Update skipped, already relaesed: {0}", System.identityHashCode(this)); + } + return; + } + if (LOG.isLoggable(Level.FINE)) { + LOG.log(Level.FINE, "Updating: {0}", System.identityHashCode(this)); + } + FoldHierarchyTransaction transaction = getOperation().openTransaction(); + try { + updateFolds(th.tokenSequence(), transaction); + } finally { + transaction.commit(); + } + } finally { + hierarchy.unlock(); + } + } + } finally { + ((BaseDocument) doc).readUnlock(); + } + } + + private void removeFoldNotify(Fold removedFold) { + if (removedFoldList == null) { + removedFoldList = new ArrayList(3); + } + removedFoldList.add(removedFold); + } + + private void removeAffectedMarks(DocumentEvent evt, FoldHierarchyTransaction transaction) { + int removeOffset = evt.getOffset(); + int markIndex = findMarkIndex(removeOffset); + if (markIndex < getMarkCount()) { + FoldMarkInfo mark; + while (markIndex >= 0 && (mark = getMark(markIndex)).getOffset() == removeOffset) { + mark.release(false, transaction); + removeMark(markIndex); + markIndex--; + } + } + } + + private void processRemovedFolds(FoldHierarchyTransaction transaction) { + if (removedFoldList != null) { + for (int i = removedFoldList.size() - 1; i >= 0; i--) { + Fold removedFold = (Fold)removedFoldList.get(i); + FoldMarkInfo startMark = (FoldMarkInfo)getOperation().getExtraInfo(removedFold); + if (startMark.getId() != null) + customFoldId.put(startMark.getId(), Boolean.valueOf(removedFold.isCollapsed())); // remember the last fold's state before remove + FoldMarkInfo endMark = startMark.getPairMark(); // get prior releasing + if (getOperation().isStartDamaged(removedFold)) { // start mark area was damaged + startMark.release(true, transaction); // forced remove + } + if (getOperation().isEndDamaged(removedFold)) { + endMark.release(true, transaction); + } + } + } + removedFoldList = null; + } + + private void markUpdate(FoldMarkInfo mark) { + markUpdate(mark.getOffset()); + } + + private void markUpdate(int offset) { + if (offset < minUpdateMarkOffset) { + minUpdateMarkOffset = offset; + } + if (offset > maxUpdateMarkOffset) { + maxUpdateMarkOffset = offset; + } + } + + private FoldMarkInfo getMark(int index) { + return (FoldMarkInfo)markArray.getItem(index); + } + + private int getMarkCount() { + return markArray.getItemCount(); + } + + private void removeMark(int index) { + if (LOG.isLoggable(Level.FINE)) { + LOG.fine("Removing mark from ind=" + index + ": " + getMark(index)); // NOI18N + } + markArray.remove(index, 1); + } + + private void insertMark(int index, FoldMarkInfo mark) { + markArray.insertItem(index, mark); + if (LOG.isLoggable(Level.FINE)) { + LOG.fine("Inserted mark at ind=" + index + ": " + mark); // NOI18N + } + } + + private int findMarkIndex(int offset) { + int markCount = getMarkCount(); + int low = 0; + int high = markCount - 1; + + while (low <= high) { + int mid = (low + high) / 2; + int midMarkOffset = getMark(mid).getOffset(); + + if (midMarkOffset < offset) { + low = mid + 1; + } else if (midMarkOffset > offset) { + high = mid - 1; + } else { + // mark starting exactly at the given offset found + // If multiple -> find the one with highest index + mid++; + while (mid < markCount && getMark(mid).getOffset() == offset) { + mid++; + } + mid--; + return mid; + } + } + return low; // return higher index (e.g. for insert) + } + + private List getMarkList(TokenSequence seq) { + List markList = null; + + for(seq.moveStart(); seq.moveNext(); ) { + Token token = seq.token(); + FoldMarkInfo info; + try { + info = scanToken(token); + } catch (BadLocationException e) { + LOG.log(Level.WARNING, null, e); + info = null; + } + + if (info != null) { + if (markList == null) { + markList = new ArrayList(); + } + markList.add(info); + } + } + + return markList; + } + + private void processTokenList(TokenSequence seq, FoldHierarchyTransaction transaction) { + List markList = getMarkList(seq); + int markListSize; + if (markList != null && ((markListSize = markList.size()) > 0)) { + // Find the index for insertion + int offset = ((FoldMarkInfo)markList.get(0)).getOffset(); + int arrayMarkIndex = findMarkIndex(offset); + // Remember the corresponding mark in the array as well + FoldMarkInfo arrayMark; + int arrayMarkOffset; + if (arrayMarkIndex < getMarkCount()) { + arrayMark = getMark(arrayMarkIndex); + arrayMarkOffset = arrayMark.getOffset(); + } else { // at last mark + arrayMark = null; + arrayMarkOffset = Integer.MAX_VALUE; + } + + for (int i = 0; i < markListSize; i++) { + FoldMarkInfo listMark = (FoldMarkInfo)markList.get(i); + int listMarkOffset = listMark.getOffset(); + if (i == 0 || i == markListSize - 1) { + // Update the update-offsets by the first and last marks in the list + markUpdate(listMarkOffset); + } + while (listMarkOffset >= arrayMarkOffset) { + if (listMarkOffset == arrayMarkOffset) { + // At the same offset - likely the same mark + // -> retain the collapsed state + listMark.setCollapsed(arrayMark.isCollapsed()); + } + if (!arrayMark.isReleased()) { // make sure that the mark is released + arrayMark.release(false, transaction); + } + removeMark(arrayMarkIndex); + if (LOG.isLoggable(Level.FINE)) { + LOG.fine("Removed dup mark from ind=" + arrayMarkIndex + ": " + arrayMark); // NOI18N + } + if (arrayMarkIndex < getMarkCount()) { + arrayMark = getMark(arrayMarkIndex); + arrayMarkOffset = arrayMark.getOffset(); + } else { // no more marks + arrayMark = null; + arrayMarkOffset = Integer.MAX_VALUE; + } + } + // Insert the listmark + insertMark(arrayMarkIndex, listMark); + if (LOG.isLoggable(Level.FINE)) { + LOG.fine("Inserted mark at ind=" + arrayMarkIndex + ": " + listMark); // NOI18N + } + arrayMarkIndex++; + } + } + } + + private void updateFolds(TokenSequence seq, FoldHierarchyTransaction transaction) { + + if (seq != null && !seq.isEmpty()) { + processTokenList(seq, transaction); + } + + if (maxUpdateMarkOffset == -1) { // no updates + return; + } + + // Find the first mark to update and init the prevMark and parentMark prior the loop + int index = findMarkIndex(minUpdateMarkOffset); + FoldMarkInfo prevMark; + FoldMarkInfo parentMark; + if (index == 0) { // start from begining + prevMark = null; + parentMark = null; + } else { + prevMark = getMark(index - 1); + parentMark = prevMark.getParentMark(); + } + + // Iterate through the changed marks in the mark array + int markCount = getMarkCount(); + while (index < markCount) { // process the marks + FoldMarkInfo mark = getMark(index); + + // If the mark was released then it must be removed + if (mark.isReleased()) { + if (LOG.isLoggable(Level.FINE)) { + LOG.fine("Removing released mark at ind=" + index + ": " + mark); // NOI18N + } + removeMark(index); + markCount--; + continue; + } + + // Update mark's status (folds, parentMark etc.) + if (mark.isStartMark()) { // starting a new fold + if (prevMark == null || prevMark.isStartMark()) { // new level + mark.setParentMark(prevMark); // prevMark == null means root level + parentMark = prevMark; + + } // same level => parent to the parent of the prevMark + + } else { // end mark + if (prevMark != null) { + if (prevMark.isStartMark()) { // closing nearest fold + prevMark.setEndMark(mark, false, transaction); + + } else { // prevMark is end mark - closing its parent fold + if (parentMark != null) { + // mark's parent gets set as well + parentMark.setEndMark(mark, false, transaction); + parentMark = parentMark.getParentMark(); + + } else { // prevMark's parentMark is null (top level) + mark.makeSolitaire(false, transaction); + } + } + + } else { // prevMark is null + mark.makeSolitaire(false, transaction); + } + } + + // Set parent mark of the mark + mark.setParentMark(parentMark); + + + prevMark = mark; + index++; + } + + minUpdateMarkOffset = Integer.MAX_VALUE; + maxUpdateMarkOffset = -1; + + if (LOG.isLoggable(Level.FINE)) { + LOG.fine("MARKS DUMP:\n" + this); //NOI18N + } + } + + public @Override String toString() { + StringBuffer sb = new StringBuffer(); + int markCount = getMarkCount(); + int markCountDigitCount = Integer.toString(markCount).length(); + for (int i = 0; i < markCount; i++) { + sb.append("["); // NOI18N + String iStr = Integer.toString(i); + appendSpaces(sb, markCountDigitCount - iStr.length()); + sb.append(iStr); + sb.append("]:"); // NOI18N + FoldMarkInfo mark = getMark(i); + + // Add extra indent regarding the depth in hierarchy + int indent = 0; + FoldMarkInfo parentMark = mark.getParentMark(); + while (parentMark != null) { + indent += 4; + parentMark = parentMark.getParentMark(); + } + appendSpaces(sb, indent); + + sb.append(mark); + sb.append('\n'); + } + return sb.toString(); + } + + private static void appendSpaces(StringBuffer sb, int spaces) { + while (--spaces >= 0) { + sb.append(' '); + } + } + + private static Pattern pattern = Pattern.compile( + "(<\\s*editor-fold" + + // id="x"[opt] defaultstate="y"[opt] desc="z"[opt] defaultstate="a"[opt] + // id must be first, the rest of attributes in random order + "(?:(?:\\s+id=\"(\\S*)\")?(?:\\s+defaultstate=\"(\\S*?)\")?(?:\\s+desc=\"([\\S \\t]*?)\")?(?:\\s+defaultstate=\"(\\S*?)\")?)" + + "\\s*>)|(?:)"); // NOI18N + + private FoldMarkInfo scanToken(Token token) throws BadLocationException { + // ignore any token that is not comment + if (token.id().primaryCategory() != null && token.id().primaryCategory().startsWith(tokenId)) { //NOI18N + Matcher matcher = pattern.matcher(token.text()); + if (matcher.find()) { + if (matcher.group(1) != null) { // fold's start mark found + boolean state; + if (matcher.group(3) != null) { + state = "collapsed".equals(matcher.group(3)); // remember the defaultstate // NOI18N + } else { + state = "collapsed".equals(matcher.group(5)); + } + + if (matcher.group(2) != null) { // fold's id exists + Boolean collapsed = (Boolean)customFoldId.get(matcher.group(2)); + if (collapsed != null) + state = collapsed.booleanValue(); // fold's state is already known from the past + else + customFoldId.put(matcher.group(2), Boolean.valueOf(state)); + } + return new FoldMarkInfo(true, token.offset(null), matcher.end(0), matcher.group(2), state, matcher.group(4)); // NOI18N + } else { // fold's end mark found + return new FoldMarkInfo(false, token.offset(null), matcher.end(0), null, false, null); + } + } + } + return null; + } + + private final class FoldMarkInfo { + + private boolean startMark; + private Position pos; + private int length; + private String id; + private boolean collapsed; + private String description; + + /** Matching pair mark used for fold construction */ + private FoldMarkInfo pairMark; + + /** Parent mark defining nesting in the mark hierarchy. */ + private FoldMarkInfo parentMark; + + /** + * Fold that corresponds to this mark (if it's start mark). + * It can be null if this mark is end mark or if it currently + * does not have the fold assigned. + */ + private Fold fold; + + private boolean released; + + private FoldMarkInfo(boolean startMark, int offset, + int length, String id, boolean collapsed, String description) + throws BadLocationException { + + this.startMark = startMark; + this.pos = doc.createPosition(offset); + this.length = length; + this.id = id; + this.collapsed = collapsed; + this.description = description; + } + + public String getId() { + return id; + } + + public String getDescription() { + return description; + } + + public boolean isStartMark() { + return startMark; + } + + public int getLength() { + return length; + } + + public int getOffset() { + return pos.getOffset(); + } + + public int getEndOffset() { + return getOffset() + getLength(); + } + + public boolean isCollapsed() { + return (fold != null) ? fold.isCollapsed() : collapsed; + } + + public boolean hasFold() { + return (fold != null); + } + + public void setCollapsed(boolean collapsed) { + this.collapsed = collapsed; + } + + public boolean isSolitaire() { + return (pairMark == null); + } + + public void makeSolitaire(boolean forced, FoldHierarchyTransaction transaction) { + if (!isSolitaire()) { + if (isStartMark()) { + setEndMark(null, forced, transaction); + } else { // end mark + getPairMark().setEndMark(null, forced, transaction); + } + } + } + + public boolean isReleased() { + return released; + } + + /** + * Release this mark and mark for update. + */ + public void release(boolean forced, FoldHierarchyTransaction transaction) { + if (!released) { + makeSolitaire(forced, transaction); + released = true; + markUpdate(this); + } + } + + public FoldMarkInfo getPairMark() { + return pairMark; + } + + private void setPairMark(FoldMarkInfo pairMark) { + this.pairMark = pairMark; + } + + public void setEndMark(FoldMarkInfo endMark, boolean forced, + FoldHierarchyTransaction transaction) { + if (!isStartMark()) { + throw new IllegalStateException("Not start mark"); // NOI18N + } + if (pairMark == endMark) { + return; + } + + if (pairMark != null) { // is currently paired to an end mark + releaseFold(forced, transaction); + pairMark.setPairMark(null); + } + + pairMark = endMark; + if (endMark != null) { + if (!endMark.isSolitaire()) { // make solitaire first + endMark.makeSolitaire(false, transaction); // not forced here + } + endMark.setPairMark(this); + endMark.setParentMark(this.getParentMark()); + ensureFoldExists(transaction); + } + } + + public FoldMarkInfo getParentMark() { + return parentMark; + } + + public void setParentMark(FoldMarkInfo parentMark) { + this.parentMark = parentMark; + } + + private void releaseFold(boolean forced, FoldHierarchyTransaction transaction) { + if (isSolitaire() || !isStartMark()) { + throw new IllegalStateException(); + } + + if (fold != null) { + setCollapsed(fold.isCollapsed()); // serialize the collapsed info + if (!forced) { + getOperation().removeFromHierarchy(fold, transaction); + } + fold = null; + } + } + + public Fold getFold() { + if (isSolitaire()) { + return null; + } + if (!isStartMark()) { + return pairMark.getFold(); + } + return fold; + } + + public void ensureFoldExists(FoldHierarchyTransaction transaction) { + if (isSolitaire() || !isStartMark()) { + throw new IllegalStateException(); + } + + if (fold == null) { + try { + if (!startMark) { + throw new IllegalStateException("Not start mark: " + this); // NOI18N + } + if (pairMark == null) { + throw new IllegalStateException("No pairMark for mark:" + this); // NOI18N + } + int startOffset = getOffset(); + int startGuardedLength = getLength(); + int endGuardedLength = pairMark.getLength(); + int endOffset = pairMark.getOffset() + endGuardedLength; + fold = getOperation().addToHierarchy( + CUSTOM_FOLD_TYPE, getDescription(), collapsed, + startOffset, endOffset, + startGuardedLength, endGuardedLength, + this, + transaction + ); + } catch (BadLocationException e) { + LOG.log(Level.WARNING, null, e); + } + } + } + + public @Override String toString() { + StringBuffer sb = new StringBuffer(); + sb.append(isStartMark() ? 'S' : 'E'); // NOI18N + + // Check whether this mark (or its pair) has fold + if (hasFold() || (!isSolitaire() && getPairMark().hasFold())) { + sb.append("F"); // NOI18N + + // Check fold's status + if (isStartMark() && (isSolitaire() + || getOffset() != fold.getStartOffset() + || getPairMark().getEndOffset() != fold.getEndOffset()) + ) { + sb.append("!!<"); // NOI18N + sb.append(fold.getStartOffset()); + sb.append(","); // NOI18N + sb.append(fold.getEndOffset()); + sb.append(">!!"); // NOI18N + } + } + + // Append mark's internal status + sb.append(" ("); // NOI18N + sb.append("o="); // NOI18N + sb.append(pos.getOffset()); + sb.append(", l="); // NOI18N + sb.append(length); + sb.append(", d='"); // NOI18N + sb.append(description); + sb.append('\''); + if (getPairMark() != null) { + sb.append(", <->"); // NOI18N + sb.append(getPairMark().getOffset()); + } + if (getParentMark() != null) { + sb.append(", ^"); // NOI18N + sb.append(getParentMark().getOffset()); + } + sb.append(')'); + + return sb.toString(); + } + + } + + public static final class Factory implements FoldManagerFactory { + + public FoldManager createFoldManager() { + return new CustomFoldManager(); + } + } +} diff --git a/editor.fold/src/org/netbeans/modules/editor/fold/ApiPackageAccessor.java b/editor.fold/src/org/netbeans/modules/editor/fold/ApiPackageAccessor.java --- a/editor.fold/src/org/netbeans/modules/editor/fold/ApiPackageAccessor.java +++ b/editor.fold/src/org/netbeans/modules/editor/fold/ApiPackageAccessor.java @@ -44,6 +44,7 @@ package org.netbeans.modules.editor.fold; +import org.netbeans.modules.editor.fold.ui.FoldViewFactory; import javax.swing.event.DocumentEvent; import javax.swing.text.BadLocationException; import javax.swing.text.Document; @@ -142,5 +143,7 @@ public abstract void foldStateChangeEndOffsetChanged(FoldStateChange fsc, int originalEndOffset); + + public abstract FoldHierarchyExecution foldGetExecution(FoldHierarchy fh); } diff --git a/editor.lib/src/org/netbeans/editor/CustomFoldManager.java b/editor.fold/src/org/netbeans/modules/editor/fold/CustomFoldManager.java rename from editor.lib/src/org/netbeans/editor/CustomFoldManager.java rename to editor.fold/src/org/netbeans/modules/editor/fold/CustomFoldManager.java --- a/editor.lib/src/org/netbeans/editor/CustomFoldManager.java +++ b/editor.fold/src/org/netbeans/modules/editor/fold/CustomFoldManager.java @@ -42,7 +42,7 @@ * made subject to such option by the copyright holder. */ -package org.netbeans.editor; +package org.netbeans.modules.editor.fold; import javax.swing.text.Document; import javax.swing.text.BadLocationException; @@ -59,10 +59,12 @@ import org.netbeans.api.lexer.Token; import org.netbeans.api.lexer.TokenHierarchy; import org.netbeans.api.lexer.TokenSequence; +import org.netbeans.editor.BaseDocument; import org.netbeans.spi.editor.fold.FoldHierarchyTransaction; import org.netbeans.spi.editor.fold.FoldManager; import org.netbeans.spi.editor.fold.FoldManagerFactory; import org.netbeans.spi.editor.fold.FoldOperation; +import org.openide.util.Parameters; import org.openide.util.RequestProcessor; /** @@ -72,7 +74,7 @@ * @version 1.00 */ -final class CustomFoldManager implements FoldManager, Runnable { +public final class CustomFoldManager implements FoldManager, Runnable { private static final Logger LOG = Logger.getLogger(CustomFoldManager.class.getName()); @@ -89,7 +91,18 @@ private static final RequestProcessor RP = new RequestProcessor(CustomFoldManager.class.getName(), 1, false, false); private final RequestProcessor.Task task = RP.create(this); + + private final String tokenId; + + public CustomFoldManager() { + this.tokenId = "comment"; + } + public CustomFoldManager(String tokenId) { + Parameters.notNull("tokenId", tokenId); + this.tokenId = tokenId; + } + public void init(FoldOperation operation) { this.operation = operation; if (LOG.isLoggable(Level.FINE)) { @@ -481,7 +494,7 @@ private FoldMarkInfo scanToken(Token token) throws BadLocationException { // ignore any token that is not comment - if (token.id().primaryCategory() != null && token.id().primaryCategory().startsWith("comment")) { //NOI18N + if (token.id().primaryCategory() != null && token.id().primaryCategory().startsWith(tokenId)) { //NOI18N Matcher matcher = pattern.matcher(token.text()); if (matcher.find()) { if (matcher.group(1) != null) { // fold's start mark found diff --git a/editor.fold/src/org/netbeans/modules/editor/fold/DefaultFoldProvider.java b/editor.fold/src/org/netbeans/modules/editor/fold/DefaultFoldProvider.java new file mode 100644 --- /dev/null +++ b/editor.fold/src/org/netbeans/modules/editor/fold/DefaultFoldProvider.java @@ -0,0 +1,83 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2013 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + * + * Contributor(s): + * + * Portions Copyrighted 2013 Sun Microsystems, Inc. + */ +package org.netbeans.modules.editor.fold; + +import java.util.ArrayList; +import java.util.Collection; +import org.netbeans.api.editor.fold.FoldType; +import org.netbeans.api.editor.mimelookup.MimeRegistration; +import org.netbeans.spi.editor.fold.FoldTypeProvider; + +/** + * Default folds, provided by the infrastructure. + * These fold types do not propagate to individual languages, only serve as + * a common base for "all languages" configuration. + * + * @author sdedic + */ +@MimeRegistration(mimeType = "", service = FoldTypeProvider.class) +public class DefaultFoldProvider implements FoldTypeProvider { + private final Collection defaultTypes; + + public DefaultFoldProvider() { + defaultTypes = new ArrayList(7); + defaultTypes.add(FoldType.CODE_BLOCK); + defaultTypes.add(FoldType.COMMENT); + defaultTypes.add(FoldType.DOCUMENTATION); + defaultTypes.add(FoldType.INITIAL_COMMENT); + defaultTypes.add(FoldType.MEMBER); + defaultTypes.add(FoldType.NESTED); + defaultTypes.add(FoldType.TAG); + defaultTypes.add(FoldType.USER); + } + + @Override + public Collection getValues(Class type) { + return type == FoldType.class ? defaultTypes : null; + } + + @Override + public boolean inheritable() { + return false; + } + +} diff --git a/editor.fold/src/org/netbeans/modules/editor/fold/FoldContentReaders.java b/editor.fold/src/org/netbeans/modules/editor/fold/FoldContentReaders.java new file mode 100644 --- /dev/null +++ b/editor.fold/src/org/netbeans/modules/editor/fold/FoldContentReaders.java @@ -0,0 +1,161 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2013 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + * + * Contributor(s): + * + * Portions Copyrighted 2013 Sun Microsystems, Inc. + */ +package org.netbeans.modules.editor.fold; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.swing.text.BadLocationException; +import javax.swing.text.Document; +import org.netbeans.api.editor.fold.Fold; +import org.netbeans.api.editor.fold.FoldTemplate; +import org.netbeans.api.editor.fold.FoldType; +import org.netbeans.api.editor.fold.FoldUtilities; +import org.netbeans.api.editor.mimelookup.MimeLookup; +import org.netbeans.spi.editor.fold.ContentReader; +import org.openide.util.Lookup; +import org.openide.util.LookupEvent; +import org.openide.util.LookupListener; + +/** + * Manages registered ContentReaders for individual Mime types. + * The cache is flushed iff the set of ContentReader.Factory changes (e.g. during module (un)installation). + * + * @author sdedic + */ +public final class FoldContentReaders { + private static FoldContentReaders INSTANCE = new FoldContentReaders(); + + private volatile Map mimeNodes = new HashMap(); + + public static FoldContentReaders get() { + return INSTANCE; + } + + public CharSequence readContent(String mime, Document d, Fold f, FoldTemplate ft) throws BadLocationException { + List readers = getReaders(mime, f.getType()); + for (ContentReader r : readers) { + CharSequence chs = r.read(d, f, ft); + if (chs != null) { + return chs; + } + } + return null; + } + + private class N implements LookupListener { + String mime; + Lookup.Result result; + Map> readers = new HashMap>(); + + public N(String mime, Lookup mimeLookup) { + this.mime = mime; + init(mimeLookup); + } + + void clear() { + result.removeLookupListener(this); + } + + @Override + public void resultChanged(LookupEvent ev) { + flush(); + } + + private void init(Lookup mimeLookup) { + Collection types = + new ArrayList((FoldUtilities.getFoldTypes(mime).values())); + + result = mimeLookup.lookupResult(ContentReader.Factory.class); + Collection factories = result.allInstances(); + + List l; + for (FoldType ft : types) { + l = null; + for (ContentReader.Factory f : factories) { + ContentReader cr = f.createReader(ft); + if (cr != null) { + if (l == null) { + l = new ArrayList(3); + } + l.add(cr); + } + } + if (l != null) { + readers.put(ft, l); + } + } + result.addLookupListener(this); + } + + List readers(FoldType ft) { + List r = readers.get(ft); + return r == null ? Collections.emptyList() : r; + } + } + + public List getReaders(String mime, FoldType ft) { + N node = mimeNodes.get(mime); + if (node == null) { + synchronized (mimeNodes) { + node = mimeNodes.get(mime); + if (node == null) { + node = new N(mime, MimeLookup.getLookup(mime)); + mimeNodes.put(mime, node); + } + } + } + return node.readers(ft); + } + + private void flush() { + synchronized (mimeNodes) { + for (N n : mimeNodes.values()) { + n.clear(); + } + mimeNodes.clear(); + } + } +} diff --git a/editor.fold/src/org/netbeans/modules/editor/fold/FoldHierarchyExecution.java b/editor.fold/src/org/netbeans/modules/editor/fold/FoldHierarchyExecution.java --- a/editor.fold/src/org/netbeans/modules/editor/fold/FoldHierarchyExecution.java +++ b/editor.fold/src/org/netbeans/modules/editor/fold/FoldHierarchyExecution.java @@ -57,8 +57,10 @@ import java.util.List; import java.util.Map; import java.util.Set; -import java.util.logging.Level; +import java.util.concurrent.TimeUnit; import java.util.logging.Logger; +import java.util.prefs.PreferenceChangeEvent; +import java.util.prefs.PreferenceChangeListener; import java.util.prefs.Preferences; import javax.swing.SwingUtilities; import javax.swing.event.DocumentEvent; @@ -67,13 +69,17 @@ import javax.swing.text.AbstractDocument; import javax.swing.text.BadLocationException; import javax.swing.text.Document; +import javax.swing.text.EditorKit; import javax.swing.text.JTextComponent; import org.netbeans.api.editor.fold.Fold; import org.netbeans.api.editor.fold.FoldHierarchy; import org.netbeans.api.editor.fold.FoldHierarchyEvent; import org.netbeans.api.editor.fold.FoldHierarchyListener; import org.netbeans.api.editor.fold.FoldStateChange; +import org.netbeans.api.editor.fold.FoldType; +import org.netbeans.api.editor.fold.FoldUtilities; import org.netbeans.api.editor.mimelookup.MimeLookup; +import org.netbeans.api.editor.mimelookup.MimePath; import org.netbeans.lib.editor.util.swing.DocumentListenerPriority; import org.netbeans.lib.editor.util.swing.DocumentUtilities; import org.netbeans.spi.editor.fold.FoldManager; @@ -83,6 +89,7 @@ import org.openide.ErrorManager; import org.openide.util.RequestProcessor; import org.openide.util.Task; +import org.openide.util.WeakListeners; /** * Class backing the FoldHierarchy in one-to-one relationship. @@ -117,7 +124,7 @@ private static final String PROPERTY_FOLD_HIERARCHY_MUTEX = "foldHierarchyMutex"; //NOI18N - private static final String PROPERTY_FOLDING_ENABLED = "code-folding-enable"; //NOI18N + private static final String PROPERTY_FOLDING_ENABLED = FoldUtilitiesImpl.PREF_CODE_FOLDING_ENABLED; //NOI18N private static final boolean debug = Boolean.getBoolean("netbeans.debug.editor.fold"); //NOI18N @@ -174,6 +181,10 @@ private Task initTask; + private Preferences foldPreferences; + + private FoldingEditorSupport editSupport; + public static synchronized FoldHierarchy getOrCreateFoldHierarchy(JTextComponent component) { return getOrCreateFoldExecution(component).getHierarchy(); } @@ -256,6 +267,9 @@ } catch (BadLocationException e) { ErrorManager.getDefault().notify(e); } + + editSupport = new FoldingEditorSupport(hierarchy, component); + } /* testing only */ @@ -263,6 +277,11 @@ getOrCreateFoldExecution(panel).getInitTask().waitFinished(); } + /* testing only */ + static boolean waitAllTasks() throws InterruptedException { + return RP.awaitTermination(30, TimeUnit.SECONDS); + } + private Task getInitTask() { return initTask; } @@ -314,6 +333,10 @@ mutex.lock(); } + public final boolean isLockedByCaller() { + return mutex.getLockThread() == Thread.currentThread(); + } + /** * Unlock the hierarchy from exclusive use. This method must only * be used together with {@link #lock()} in try..finally block. @@ -454,6 +477,10 @@ : null; } + Set getBlockedFolds(Fold f) { + return (Set)block2blockedSet.get(f); + } + /** * Mark given fold as blocked by the block fold. */ @@ -689,6 +716,18 @@ } } } + + private String getMimeType() { + EditorKit ek = component.getUI().getEditorKit(component); + String mimeType; + + if (ek != null) { + mimeType = ek.getContentType(); + } else { + mimeType = ""; + } + return mimeType; + } /** * Rebuild (or release) the root folds of the hierarchy in the event dispatch thread. @@ -721,12 +760,22 @@ boolean ok = false; try { operations = new FoldOperationImpl[factoryListLength]; - for (int i = 0; i < factoryListLength; i++) { + int i; + for (i = 0; i < factoryListLength; i++) { FoldManagerFactory factory = (FoldManagerFactory)factoryList.get(i); FoldManager manager = factory.createFoldManager(); + if (manager == null) { + continue; + } operations[i] = new FoldOperationImpl(this, manager, priority); priority--; } + // trim the array in the unlikely case + if (i < factoryListLength) { + FoldOperationImpl[] ops = new FoldOperationImpl[i]; + System.arraycopy(operations, 0, ops, 0, i); + operations = ops; + } ok = true; } finally { if (!ok) { @@ -878,7 +927,7 @@ if (b == null && component.getDocument() != null) { String mime = DocumentUtilities.getMimeType(component.getDocument()); if (mime != null) { - Preferences prefs = MimeLookup.getLookup(mime).lookup(Preferences.class); + Preferences prefs = getFoldPreferences(); b = prefs.getBoolean(PROPERTY_FOLDING_ENABLED, true); } } @@ -996,5 +1045,70 @@ return sb.toString(); } + + public boolean hasProviders() { + return operations.length > 0; + } + + /** + * Cache for initial states for individual FoldTypes. + * The cache is invalidated iff the Preferences change in a key which starts with the collapse- prefix. + * The cache is NOT invalidated on the FoldType set change, as if the foldtype set changes, either new FoldTypes + * appear (they will enter the cache eventually), or the obsolete FoldTypes will not be used in the future, + * so they may rote in the cache until the Component is closed. + */ + private volatile Map initialFoldState = new HashMap(); + + /** + * Listener on fold preferences. + */ + private PreferenceChangeListener weakPrefL; + + /** + * Returns the cached value for initial folding state of the specific type. The method may be only + * called under a lock, since it populates the cache; no concurrency is permitted. + * + * @param ft the FoldType to inspect + * @return true, if the fold should be collapsed initially. + */ + public boolean getInitialFoldState(FoldType ft) { + Boolean b = initialFoldState.get(ft); + if (b != null) { + return b; + } + b = FoldUtilities.isAutoCollapsed(ft, hierarchy); + initialFoldState.put(ft, b); + return b; + } + + /** + * Obtains Preferences that control folding for this Hierarchy. + * + * @return Preferences object + */ + public Preferences getFoldPreferences() { + if (foldPreferences == null) { + String mimeType = getMimeType(); + // internally does MimeLookup lookup(Preferences.class) + Preferences prefs = LegacySettingsSync.get().processMime(mimeType); + if ("".equals(mimeType)) { + // do not cache; typically the editor kit will be changed to something other + return prefs; + } + foldPreferences = prefs; + weakPrefL = WeakListeners.create(PreferenceChangeListener.class, new PreferenceChangeListener() { + @Override + public void preferenceChange(PreferenceChangeEvent evt) { + if (evt.getKey().startsWith(FoldUtilitiesImpl.PREF_COLLAPSE_PREFIX)) { + if (!initialFoldState.isEmpty()) { + initialFoldState = new HashMap(); + } + } + } + }, foldPreferences); + foldPreferences.addPreferenceChangeListener(weakPrefL); + } + return foldPreferences; + } } diff --git a/editor.fold/src/org/netbeans/modules/editor/fold/FoldHierarchyTransactionImpl.java b/editor.fold/src/org/netbeans/modules/editor/fold/FoldHierarchyTransactionImpl.java --- a/editor.fold/src/org/netbeans/modules/editor/fold/FoldHierarchyTransactionImpl.java +++ b/editor.fold/src/org/netbeans/modules/editor/fold/FoldHierarchyTransactionImpl.java @@ -242,7 +242,7 @@ updateAffectedOffsets(fold); int startOffset = change.getOriginalStartOffset(); int endOffset = change.getOriginalEndOffset(); - assert (startOffset <= endOffset) : "startOffset=" + startOffset + " > endOffset=" + endOffset; // NOI18N; + assert (endOffset < 0 || startOffset <= endOffset) : "startOffset=" + startOffset + " > endOffset=" + endOffset; // NOI18N; if (startOffset != -1) { updateAffectedStartOffset(startOffset); } diff --git a/editor.fold/src/org/netbeans/modules/editor/fold/FoldOperationImpl.java b/editor.fold/src/org/netbeans/modules/editor/fold/FoldOperationImpl.java --- a/editor.fold/src/org/netbeans/modules/editor/fold/FoldOperationImpl.java +++ b/editor.fold/src/org/netbeans/modules/editor/fold/FoldOperationImpl.java @@ -44,16 +44,39 @@ package org.netbeans.modules.editor.fold; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Set; +import java.util.Stack; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.prefs.Preferences; import javax.swing.event.DocumentEvent; import javax.swing.text.BadLocationException; import javax.swing.text.Document; +import javax.swing.text.EditorKit; +import javax.swing.text.JTextComponent; import org.netbeans.spi.editor.fold.FoldManager; import org.netbeans.api.editor.fold.Fold; import org.netbeans.api.editor.fold.FoldHierarchy; +import org.netbeans.spi.editor.fold.FoldInfo; +import org.netbeans.api.editor.fold.FoldStateChange; +import org.netbeans.api.editor.fold.FoldTemplate; import org.netbeans.spi.editor.fold.FoldOperation; import org.netbeans.api.editor.fold.FoldType; +import org.netbeans.api.editor.mimelookup.MimeLookup; +import org.netbeans.editor.BaseDocument; +import org.netbeans.spi.editor.fold.FoldHierarchyTransaction; +import org.openide.util.Exceptions; /** @@ -204,6 +227,9 @@ throws BadLocationException { checkFoldOperation(fold); int origEndOffset = fold.getEndOffset(); + if (origEndOffset == endOffset) { + return; + } ApiPackageAccessor api = getAccessor(); api.foldSetEndOffset(fold, getDocument(), endOffset); api.foldStateChangeEndOffsetChanged(transaction.getFoldStateChange(fold), origEndOffset); @@ -235,7 +261,16 @@ public boolean isReleased() { return released; } - + + /** + * Enumerates all folds contributed by this Operation, whether blocked or active. + * + * @return + */ + public Iterator foldIterator() { + return new BI(new DFSI(execution.getRootFold())); + } + private void checkFoldOperation(Fold fold) { FoldOperationImpl foldOperation = getAccessor().foldGetOperation(fold); if (foldOperation != this) { @@ -250,5 +285,446 @@ private static ApiPackageAccessor getAccessor() { return ApiPackageAccessor.get(); } + + /** + * Compares two folds A, B. Fold "A" precedes "B", if and only if: + *

    + *
  • A fully encloses B, or + *
  • A starts before B, or + *
  • A and B occupy the same range, and A's priority is lower + *
+ * + * @param a fold to compare + * @param b fold to compare + * @return -1, 1, 0 as appropriate for a Comparator + */ + private static final Comparator FOLD_COMPARATOR = new Comparator() { + @Override + public int compare(Fold a, Fold b) { + int diff = a.getStartOffset() - b.getStartOffset(); + if (diff < 0) { + return -1; + } + int diff2 = b.getEndOffset() - a.getEndOffset(); + if (diff2 != 0 || diff != 0) { + return 1; + } + ApiPackageAccessor accessor = getAccessor(); + return accessor.foldGetOperation(a).getPriority() - accessor.foldGetOperation(b).getPriority(); + } + }; + /** + * Level of depth-first traversal + */ + static class PS { + private Fold parent; + private int childIndex = -1; + private PS next; + + PS(Fold parent, PS next) { + this.parent = parent; + this.next = next; + } + } + + /** + * Implmentation of depth-first pre-order traversal through Fold hierarchy. + * Each level is iterated in the fold order = start offset order. + */ + private class DFSI implements Iterator { + PS level; + + private DFSI(Fold root) { + level = new PS(root, null); + } + + @Override + public Fold next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + if (level.childIndex == -1) { + level.childIndex++; + return level.parent; + } + // note that hasNext also pops levels, as necessary, so there's a + // level, which is not yet exhausted. + Fold f = level.parent.getFold(level.childIndex++); + if (f.getFoldCount() > 0) { + level = new PS(f, level); + level.childIndex++; + return level.parent; + } + return f; + } + + @Override + public boolean hasNext() { + while (level != null) { + if (level.childIndex == -1) { + return true; + } else if (level.childIndex >= level.parent.getFoldCount()) { + level = level.next; + } else { + return true; + } + } + return false; + } + + public void remove() { + throw new IllegalArgumentException(); + } + } + + /** + * Iterator, which processes all blocked folds along with their blocker. + * The blocker + blockers folds are ordered using the fold order. Results + * are filtered to contain just Folds produced by this Operation. Folds + * owned by other operations/executions are skipped. + *

+ * Note that blocked folds do not form a hierarchy; they were removed from + * the fold hierarchy when it was decided to block those folds. So prior to + * iterating further in FoldHierarchy, all (recursively) blocked folds must + * be processed. + */ + private class BI implements Iterator { + private Iterator dfsi; + private Iterator blockedFolds; + private Fold ret; + private Stack blockStack = new Stack(); + private Fold blocker; + + public BI(Iterator dfsi) { + this.dfsi = dfsi; + } + + /** + * If fold 'f' blocks some other folds, those blocked folds will b processed + * instead of 'f'. f will be mixed among and ordered with its blocked folds, so the + * entire chain will be processed in the document order. + * + * @param f + * @return true, if blocked folds should be processed. + */ + private boolean processBlocked(Fold f) { + if (f == blocker) { + return false; + } + Collection blocked = execution.getBlockedFolds(f); + if (blocked != null && !blocked.isEmpty()) { + List blockedSorted = new ArrayList(blocked.size() + 1); + blockedSorted.addAll(blocked); + // enumerate together with blocked ones + blockedSorted.add(f); + Collections.sort(blockedSorted, FOLD_COMPARATOR); + blockStack.push(new Object[] { blockedFolds, blocker}); + blockedFolds = blockedSorted.iterator(); + blocker = f; + return true; + } else { + return false; + } + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + + @Override + public Fold next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + Fold f = ret; + ret = null; + return f; + } + + @Override + public boolean hasNext() { + if (ret != null) { + return true; + } + if (blockedFolds != null) { + while (blockedFolds.hasNext()) { + Fold f = blockedFolds.next(); + if (processBlocked(f)) { + // continue with a different level of blocking + continue; + } + if (operation.owns(f)) { + ret = f; + return true; + } + } + blockedFolds = null; + } + if (!blockStack.isEmpty()) { + Object[] o = blockStack.pop(); + blocker = (Fold)o[1]; + blockedFolds = (Iterator)o[0]; + return hasNext(); + } + + while (dfsi.hasNext()) { + Fold f = dfsi.next(); + if (processBlocked(f)) { + return hasNext(); + } + if (operation.owns(f)) { + ret = f; + return true; + } + } + return false; + } + } + + public Map update(Collection fi, Collection removed, Collection created) throws BadLocationException { + Refresher r = new Refresher(fi); + if (!isReleased()) { + if (!execution.isLockedByCaller()) { + throw new IllegalStateException("Update must run under FoldHierarchy lock"); + } + r.run(); + } else { + return null; + } + if (removed != null) { + removed.addAll(r.toRemove); + } + if (created != null) { + created.addAll(r.toAdd); + } + return r.currentFolds; + } + + public boolean getInitialState(FoldType ft) { + return execution.getInitialFoldState(ft); + } + + private class Refresher implements Comparator { + private Collection foldInfos; + private Collection toRemove = new ArrayList(); + private Collection toAdd = new ArrayList(); + private Map currentFolds = new HashMap(); + + /** + * Transaction which covers the update + */ + private FoldHierarchyTransactionImpl tran; + + public Refresher(Collection foldInfos) { + this.foldInfos = foldInfos; + } + + @Override + public int compare(FoldInfo a, FoldInfo b) { + int diff = a.getStart() - b.getStart(); + if (diff < 0) { + return -1; + } + int diff2 = b.getEnd() - a.getEnd(); + if (diff2 != 0 || diff != 0) { + return 1; + } + return 0; + } + + + private int compare(FoldInfo info, Fold f) { + if (info == null) { + if (f == null) { + return 0; + } else { + return 1; + } + } else if (f == null) { + return -1; + } + + int diff = info.getStart() - f.getStartOffset(); + if (diff != 0) { + return diff; + } + diff = f.getEndOffset() - info.getEnd(); + if (diff != 0) { + return diff; + } + if (info.getType() == f.getType()) { + return 0; + } + return info.getType().code().compareToIgnoreCase(f.getType().code()); + } + + private Iterator foldIt; + private Iterator infoIt; + private FoldInfo nextInfo; + + private FoldInfo ni() { + if (nextInfo != null) { + FoldInfo f = nextInfo; + nextInfo = null; + return f; + } + return infoIt.hasNext() ? infoIt.next() : null; + } + + private FoldInfo peek() { + FoldInfo f = ni(); + nextInfo = f; + return f; + } + + private boolean containsOneAnother(FoldInfo i, Fold f) { + int s1 = i.getStart(); + int s2 = f.getStartOffset(); + int e1 = i.getEnd(); + int e2 = f.getEndOffset(); + + return ((s1 >= s2 && e2 >= e1) || + (s2 >= s1 && e1 >= e2)); + } + + private boolean nextSameRange(FoldInfo i, Fold f) { + if (i == null || f == null) { + return false; + } + if (i.getType() != f.getType() || !containsOneAnother(i, f)) { + return false; + } + FoldInfo next = peek(); + if (next == null) { + return true; + } + return next.getStart() != f.getStartOffset() || next.getEnd() != f.getEndOffset(); + } + + private boolean containsSame(FoldInfo i, Fold f) { + if (i == null || f == null || i.getType() != f.getType()) { + return false; + } + return containsOneAnother(i, f); + } + + public void run() throws BadLocationException { + // first order the supplied folds: + List ll = new ArrayList(foldInfos); + Collections.sort(ll, this); + + foldIt = foldIterator(); + infoIt = ll.iterator(); + + Fold f = foldIt.hasNext() ? foldIt.next() : null; + FoldInfo i = infoIt.hasNext() ? infoIt.next() : null; + + tran = openTransaction(); + + + try { + while (f != null || i != null) { + if (LOG.isLoggable(Level.FINEST)) { + LOG.finest("Fold = " + f + ", FoldInfo = " + i); + } + int action = compare(i, f); + if (action < 0 && !nextSameRange(i, f)) { + // create a new fold from the FoldInfo + toAdd.add(i); + i = ni(); + if (LOG.isLoggable(Level.FINEST)) { + LOG.finest("Advanced info, next = " + i); + } + continue; + } else if (action > 0 && !containsSame(i, f)) { + toRemove.add(f); + f = foldIt.hasNext() ? foldIt.next() : null; + if (LOG.isLoggable(Level.FINEST)) { + LOG.finest("Advanced fold, next = " + f); + } + continue; + } + + update(f, i); + currentFolds.put(i, f); + i = ni(); + f = foldIt.hasNext() ? foldIt.next() : null; + if (LOG.isLoggable(Level.FINEST)) { + LOG.finest("Advanced both info & fold"); + } + } + for (Fold fold : toRemove) { + if (LOG.isLoggable(Level.FINEST)) { + LOG.finest("Removing: " + f); + } + if (fold.getParent() != null) { + removeFromHierarchy(fold, tran); + } + } + for (FoldInfo info : toAdd) { + try { + currentFolds.put(info, getOperation().addToHierarchy( + info.getType(), + info.getStart(), info.getEnd(), + info.getCollapsed(), + info.getTemplate(), + info.getDescriptionOverride(), + info.getExtraInfo(), + tran.getTransaction())); + if (LOG.isLoggable(Level.FINEST)) { + LOG.finest("Adding: " + i); + } + } catch (BadLocationException ex) { + Exceptions.printStackTrace(ex); + } + } + } finally { + tran.commit(); + } + } + + public Fold update(Fold f, FoldInfo info) throws BadLocationException { + this.fsch = null; + int offs = f.getStartOffset(); + ApiPackageAccessor acc = getAccessor(); + if (info.getStart() != offs) { + acc.foldSetStartOffset(f, getDocument(), info.getStart()); + acc.foldStateChangeStartOffsetChanged(getFSCH(f), offs); + } + offs = f.getEndOffset(); + if (info.getEnd() != offs) { + acc.foldSetEndOffset(f, getDocument(), info.getEnd()); + acc.foldStateChangeEndOffsetChanged(getFSCH(f), offs); + } + String desc = info.getDescriptionOverride(); + if (desc == null) { + desc = info.getTemplate().getDescription(); + } + if (!f.getDescription().equals(desc)) { + acc.foldSetDescription(f, desc); + acc.foldStateChangeDescriptionChanged(getFSCH(f)); + } + if (info.getCollapsed() != null && f.isCollapsed() != info.getCollapsed()) { + getAccessor().foldSetCollapsed(f, info.getCollapsed()); + getAccessor().foldStateChangeCollapsedChanged(getFSCH(f)); + } + return f; + } + + /** + * FoldStateChange for the current fold being updated; + * just an optimization. + */ + private FoldStateChange fsch; + + private FoldStateChange getFSCH(Fold f) { + if (fsch != null) { + return fsch; + } + return fsch = tran.getFoldStateChange(f); + } + + } } diff --git a/editor.fold/src/org/netbeans/modules/editor/fold/FoldRegistry.java b/editor.fold/src/org/netbeans/modules/editor/fold/FoldRegistry.java new file mode 100644 --- /dev/null +++ b/editor.fold/src/org/netbeans/modules/editor/fold/FoldRegistry.java @@ -0,0 +1,343 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2011 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + * + * Contributor(s): + * + * Portions Copyrighted 2011 Sun Microsystems, Inc. + */ +package org.netbeans.modules.editor.fold; + +import java.lang.ref.Reference; +import java.lang.ref.SoftReference; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import org.netbeans.api.editor.fold.FoldType; +import org.netbeans.api.editor.mimelookup.MimeLookup; +import org.netbeans.api.editor.mimelookup.MimePath; +import org.netbeans.spi.editor.fold.FoldTypeProvider; +import org.openide.util.Lookup; +import org.openide.util.Lookup.Result; +import org.openide.util.LookupEvent; +import org.openide.util.LookupListener; +import org.openide.util.WeakListeners; + +/** + * MIMEtype specific registry, which collects values for a single enum type (enumType). + * It looks up + * {@link ExtEnum.MimeEnumProvider} in the {@link MimeLookup}. It supports efficient + * valueOf lookup and changes to the set of the values. + *

+ * Clients must not cache the set of values and treat it as + * an universe. As modules are enabled / disabled, + * the set of available values may change and further queries to {@link #valueSet} + * may return different results. + * + *

+ * Note: this class was trimmed down from generic enum registry. When other extensible + * enums are introduced, fold registry will just wrap the real enum. Some constructions may + * be unnecessary general ;) + * + * @author sdedic + */ +public final class FoldRegistry { + private static final Logger LOG = Logger.getLogger(FoldRegistry.class.getName()); + + /** + * The enum type being registered + */ + private Class enumType; + + /** + * For a MimePath, holds set of applicable enums and value-to-enum map + */ + private final Map enums = new HashMap(); + + private FoldRegistry(Class enumType) { + this.enumType = enumType; + } + + private static volatile Reference INSTANCE = new WeakReference(null); + + public static FoldRegistry get() { + FoldRegistry fr = INSTANCE.get(); + if (fr == null) { + synchronized (FoldRegistry.class) { + fr = INSTANCE.get(); + if (fr == null) { + fr = new FoldRegistry(FoldType.class); + INSTANCE = new SoftReference(fr); + } + } + } + return fr; + } + + public Collection values(MimePath mime) { + return get(mime).enums; + } + + public FoldType valueOf(MimePath mime, String val) { + return get(mime).valueOf(val); + } + + public FoldType.Domain getDomain(MimePath mime) { + return get(mime); + } + + private R get(MimePath mime) { + synchronized (enums) { + R r = enums.get(mime); + if (r != null) { + return r; + } + } + + return refreshMime(mime, null); + } + + /** + * Representation of the domain for a specific MIMEtype. It listens on a Lookup.Result + * and refreshes the contents iff lookup changes - that is if a module is (un)loaded, + * or the value provider set changes. + *

+ * Updates to enums must occur under synchro + * + */ + private static class R implements LookupListener, FoldType.Domain { + final FoldRegistry dom; + final MimePath mime; + /** + * Listens on mime lookup + */ + final Lookup.Result result; + /** + * Listens on parent's mime lookup, possibly null + */ + final Lookup.Result result2; + Collection listeners; + + /** + * Set of all enum values + */ + // @GuardedBy(this) + private Set enums; + + /** + * Allows to faster map an enum to a value. Produced lazily by map(), cleared by reset(). + */ + volatile Map valueMap; + + public R(FoldRegistry dom, MimePath mime, Result result, Result result2) { + this.dom = dom; + this.mime = mime; + this.result = result; + this.result2 = result2; + result.addLookupListener(WeakListeners.create(LookupListener.class, + this, result)); + if (result2 != null) { + result2.addLookupListener(WeakListeners.create(LookupListener.class, + this, result2)); + } + } + + @Override + public void resultChanged(LookupEvent ev) { + dom.refreshMime(mime, this); + } + + @Override + public Collection values() { + return Collections.unmodifiableCollection(map().values()); + } + + private Map map() { + Map m; + + m = valueMap; + if (m == null) { + synchronized (this) { + if (valueMap != null) { + return valueMap; + } + Set vals = enums; + m = new LinkedHashMap(); + for (FoldType e : vals) { + FoldType old = m.put(e.code(), e); + if (old != null) { + throw new IllegalArgumentException("Two fold types share the same code: " + old + " and " + e); + } + } + this.valueMap = m; + } + } + return m; + } + + @Override + public FoldType valueOf(String val) { + return map().get(val); + } + + @Override + public void addChangeListener(ChangeListener l) { + synchronized (this) { + if (listeners == null) { + listeners = new ArrayList(); + } + listeners.add(l); + } + } + + @Override + public void removeChangeListener(ChangeListener l) { + synchronized (this) { + if (listeners != null) { + listeners.remove(l); + } + } + } + + void reset(Set allValues) { + ChangeListener[] ll; + synchronized (this) { + enums = allValues; + valueMap = null; + if (listeners == null) { + return; + } + ll = listeners.toArray(new ChangeListener[listeners.size()]); + } + ChangeEvent e = new ChangeEvent(this); + for (int i = 0; i < ll.length; i++) { + ll[i].stateChanged(e); + } + } + } + + + private R refreshMime(MimePath mime, R holder) { + Lookup.Result r; + Lookup.Result pr = null; + + if (holder == null) { + r = MimeLookup.getLookup(mime).lookup( + new Lookup.Template( + FoldTypeProvider.class + ) + ); + // get the inherited MimePaths: + String parentMime = mime.getInheritedType(); + if (LOG.isLoggable(Level.FINER)) { + LOG.finer("Get providers for " + mime + ", parent " + parentMime); + } + if (parentMime != null) { + pr = MimeLookup.getLookup(parentMime).lookup( + new Lookup.Template( + FoldTypeProvider.class + ) + ); + } + } else { + r = holder.result; + pr = holder.result2; + } + + + Collection providers = r.allInstances(); + Collection parentProvs = pr == null ? Collections.emptySet() : pr.allInstances(); + + for (Iterator it = parentProvs.iterator(); it.hasNext(); ) { + FoldTypeProvider p = it.next(); + if (p.inheritable()) { + if (LOG.isLoggable(Level.FINER)) { + LOG.log(Level.FINER, "Inheritable: " + p); + } + // leave only providers, which are !inheritable, so they'll be ignored. + it.remove(); + } + } + + Set allValues = new LinkedHashSet(); + + for (FoldTypeProvider p : providers) { + if (parentProvs.contains(p)) { + if (LOG.isLoggable(Level.FINER)) { + LOG.log(Level.FINER, "Removing not inheritable: " + p); + } + // provider registered for parent, but inheritance is disallowed. + continue; + } + + Collection vals = p.getValues(enumType); + if (vals != null) { + // check that none enum overrides another one + allValues.addAll(vals); + } + } + + boolean register = holder == null; + if (holder == null) { + holder = new R(this, mime, r, pr); + } + // sync + holder.reset(allValues); + + if (register) { + synchronized(enums) { + R oldHolder = enums.put(mime, holder); + if (oldHolder != null) { + // unlikely, but can happen. + enums.put(mime, oldHolder); + holder = oldHolder; + } + } + } + return holder; + } +} diff --git a/editor.fold/src/org/netbeans/modules/editor/fold/FoldToolTip.java b/editor.fold/src/org/netbeans/modules/editor/fold/FoldToolTip.java deleted file mode 100644 --- a/editor.fold/src/org/netbeans/modules/editor/fold/FoldToolTip.java +++ /dev/null @@ -1,152 +0,0 @@ -/* - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. - * - * Copyright 1997-2010 Oracle and/or its affiliates. All rights reserved. - * - * Oracle and Java are registered trademarks of Oracle and/or its affiliates. - * Other names may be trademarks of their respective owners. - * - * The contents of this file are subject to the terms of either the GNU - * General Public License Version 2 only ("GPL") or the Common - * Development and Distribution License("CDDL") (collectively, the - * "License"). You may not use this file except in compliance with the - * License. You can obtain a copy of the License at - * http://www.netbeans.org/cddl-gplv2.html - * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the - * specific language governing permissions and limitations under the - * License. When distributing the software, include this License Header - * Notice in each file and include the License file at - * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the GPL Version 2 section of the License file that - * accompanied this code. If applicable, add the following below the - * License Header, with the fields enclosed by brackets [] replaced by - * your own identifying information: - * "Portions Copyrighted [year] [name of copyright owner]" - * - * Contributor(s): - * - * The Original Software is NetBeans. The Initial Developer of the Original - * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun - * Microsystems, Inc. All Rights Reserved. - * - * If you wish your version of this file to be governed by only the CDDL - * or only the GPL Version 2, indicate your decision by adding - * "[Contributor] elects to include this software in this distribution - * under the [CDDL or GPL Version 2] license." If you do not indicate a - * single choice of license, a recipient has the option to distribute - * your version of this file under either the CDDL, the GPL Version 2 or - * to extend the choice of license to its licensees as provided above. - * However, if you add GPL Version 2 code and therefore, elected the GPL - * Version 2 license, then the option applies only if the new code is - * made subject to such option by the copyright holder. - */ - -package org.netbeans.modules.editor.fold; - -import java.awt.BorderLayout; -import java.awt.Color; -import java.awt.Dimension; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import javax.swing.JComponent; -import javax.swing.JEditorPane; -import javax.swing.JPanel; -import javax.swing.border.LineBorder; -import javax.swing.event.AncestorEvent; -import javax.swing.event.AncestorListener; -import javax.swing.text.JTextComponent; -import org.netbeans.modules.editor.lib2.view.DocumentView; -import org.openide.util.Exceptions; -import org.openide.util.Lookup; - -/** - * Component that displays a collapsed fold preview. - * - * @author Miloslav Metelka - */ -public class FoldToolTip extends JPanel { - private int editorPaneWidth; - - public FoldToolTip(JEditorPane editorPane, final JEditorPane foldPreviewPane, Color borderColor) { - setLayout(new BorderLayout()); - add(foldPreviewPane, BorderLayout.CENTER); - putClientProperty("tooltip-type", "fold-preview"); // Checked in NbToolTip - - addGlyphGutter(foldPreviewPane); - - addAncestorListener(new AncestorListener() { - @Override - public void ancestorAdded(AncestorEvent event) { - } - - @Override - public void ancestorRemoved(AncestorEvent event) { - // Deactivate the view hierarchy immediately for foldPreviewPane - final DocumentView docView = DocumentView.get(foldPreviewPane); - if (docView != null) { - docView.runTransaction(new Runnable() { - @Override - public void run() { - docView.updateLengthyAtomicEdit(+100); // Effectively disable any VH updates - } - }); - } - // Remove the listener - FoldToolTip.this.removeAncestorListener(this); - } - - @Override - public void ancestorMoved(AncestorEvent event) { - } - }); - - editorPaneWidth = editorPane.getSize().width; - - setBorder(new LineBorder(borderColor)); - setOpaque(true); - } - - private void addGlyphGutter(JTextComponent jtx) { - ClassLoader cls = Lookup.getDefault().lookup(ClassLoader.class); - Class clazz; - Class editorUiClass; - - JComponent gutter = null; - try { - clazz = Class.forName("org.netbeans.editor.GlyphGutter", true, cls); // NOI18N - editorUiClass = Class.forName("org.netbeans.editor.EditorUI", true, cls); // NOI18N - // get the factory instance - Object o = clazz.newInstance(); - Method m = clazz.getDeclaredMethod("createSideBar", JTextComponent.class); // NOI18N - gutter = (JComponent)m.invoke(o, jtx); - } catch (IllegalArgumentException ex) { - Exceptions.printStackTrace(ex); - } catch (InvocationTargetException ex) { - Exceptions.printStackTrace(ex); - } catch (NoSuchMethodException ex) { - Exceptions.printStackTrace(ex); - } catch (SecurityException ex) { - Exceptions.printStackTrace(ex); - } catch (InstantiationException ex) { - Exceptions.printStackTrace(ex); - } catch (IllegalAccessException ex) { - Exceptions.printStackTrace(ex); - } catch (ClassNotFoundException ex) { - Exceptions.printStackTrace(ex); - } - if (gutter != null) { - add(gutter, BorderLayout.WEST); - } - } - - @Override - public Dimension getPreferredSize() { - Dimension prefSize = super.getPreferredSize(); - // Return width like for editor pane which forces the PopupManager to display - // the tooltip to align exacty with the text (below/above). - prefSize.width = Math.min(prefSize.width, editorPaneWidth); - return prefSize; - } - -} diff --git a/editor.fold/src/org/netbeans/modules/editor/fold/FoldUtilitiesImpl.java b/editor.fold/src/org/netbeans/modules/editor/fold/FoldUtilitiesImpl.java --- a/editor.fold/src/org/netbeans/modules/editor/fold/FoldUtilitiesImpl.java +++ b/editor.fold/src/org/netbeans/modules/editor/fold/FoldUtilitiesImpl.java @@ -48,13 +48,16 @@ import java.util.Collection; import java.util.Iterator; import java.util.List; +import java.util.prefs.Preferences; import javax.swing.text.AbstractDocument; import javax.swing.text.Document; import org.netbeans.api.editor.fold.Fold; import org.netbeans.api.editor.fold.FoldHierarchy; import org.netbeans.api.editor.fold.FoldHierarchyEvent; import org.netbeans.api.editor.fold.FoldStateChange; +import org.netbeans.api.editor.fold.FoldType; import org.netbeans.api.editor.fold.FoldUtilities; +import org.netbeans.api.editor.mimelookup.MimeLookup; /** * Implementations of methods from {@link org.netbeans.api.editor.fold.FoldUtilities}. @@ -64,11 +67,57 @@ */ public final class FoldUtilitiesImpl { + /** + * Prefix used for initial-collapse folding preferences. + */ + public static final String PREF_COLLAPSE_PREFIX = "code-folding-collapse-"; + + /** + * Preference key name for "use defaults" (default: true) + */ + public static final String PREF_OVERRIDE_DEFAULTS = "code-folding-use-defaults"; // NOI18N + + /** + * Preference key name for enable code folding (default: true) + */ + public static final String PREF_CODE_FOLDING_ENABLED = "code-folding-enable"; // NOI18N + + /** + * Preference key for "Content preview" display option (default: true). + */ + public static final String PREF_CONTENT_PREVIEW = "code-folding-content.preview"; // NOI18N + + /** + * Preference key for "Show summary" display option (default: true). + */ + public static final String PREF_CONTENT_SUMMARY = "code-folding-content.summary"; // NOI18N private FoldUtilitiesImpl() { // No instances } + public static boolean isFoldingEnabled(String mime) { + Preferences prefs = MimeLookup.getLookup(mime).lookup(Preferences.class); + return prefs == null ? false : prefs.getBoolean(PREF_CODE_FOLDING_ENABLED, false); + } + + public static boolean isFoldingEnabled(FoldHierarchy h) { + Preferences p = ApiPackageAccessor.get().foldGetExecution(h).getFoldPreferences(); + return p.getBoolean(PREF_CODE_FOLDING_ENABLED, false); + } + + public static boolean isAutoCollapsed(FoldType ft, FoldHierarchy h) { + Preferences p = ApiPackageAccessor.get().foldGetExecution(h).getFoldPreferences(); + FoldType parent = ft.parent(); + return p.getBoolean( + PREF_COLLAPSE_PREFIX + ft.code(), + parent == null ? + false : + // search for the parent, if parent is defined. + p.getBoolean(PREF_COLLAPSE_PREFIX + parent.code(), false) + ); + } + public static void collapseOrExpand(FoldHierarchy hierarchy, Collection foldTypes, boolean collapse) { diff --git a/editor.fold/src/org/netbeans/modules/editor/fold/FoldView.java b/editor.fold/src/org/netbeans/modules/editor/fold/FoldView.java deleted file mode 100644 --- a/editor.fold/src/org/netbeans/modules/editor/fold/FoldView.java +++ /dev/null @@ -1,358 +0,0 @@ -/* - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. - * - * Copyright 1997-2010 Oracle and/or its affiliates. All rights reserved. - * - * Oracle and Java are registered trademarks of Oracle and/or its affiliates. - * Other names may be trademarks of their respective owners. - * - * The contents of this file are subject to the terms of either the GNU - * General Public License Version 2 only ("GPL") or the Common - * Development and Distribution License("CDDL") (collectively, the - * "License"). You may not use this file except in compliance with the - * License. You can obtain a copy of the License at - * http://www.netbeans.org/cddl-gplv2.html - * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the - * specific language governing permissions and limitations under the - * License. When distributing the software, include this License Header - * Notice in each file and include the License file at - * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the GPL Version 2 section of the License file that - * accompanied this code. If applicable, add the following below the - * License Header, with the fields enclosed by brackets [] replaced by - * your own identifying information: - * "Portions Copyrighted [year] [name of copyright owner]" - * - * Contributor(s): - * - * The Original Software is NetBeans. The Initial Developer of the Original - * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun - * Microsystems, Inc. All Rights Reserved. - * - * If you wish your version of this file to be governed by only the CDDL - * or only the GPL Version 2, indicate your decision by adding - * "[Contributor] elects to include this software in this distribution - * under the [CDDL or GPL Version 2] license." If you do not indicate a - * single choice of license, a recipient has the option to distribute - * your version of this file under either the CDDL, the GPL Version 2 or - * to extend the choice of license to its licensees as provided above. - * However, if you add GPL Version 2 code and therefore, elected the GPL - * Version 2 license, then the option applies only if the new code is - * made subject to such option by the copyright holder. - */ - -package org.netbeans.modules.editor.fold; - -import java.awt.Color; -import java.awt.Container; -import java.awt.Font; -import java.awt.Graphics2D; -import java.awt.Rectangle; -import java.awt.Shape; -import java.awt.font.FontRenderContext; -import java.awt.font.TextHitInfo; -import java.awt.font.TextLayout; -import java.awt.geom.Rectangle2D; -import java.util.logging.Logger; -import javax.swing.JComponent; -import javax.swing.JEditorPane; -import javax.swing.text.AttributeSet; -import javax.swing.text.BadLocationException; -import javax.swing.text.Document; -import javax.swing.text.EditorKit; -import javax.swing.text.Element; -import javax.swing.text.JTextComponent; -import javax.swing.text.Position; -import javax.swing.text.Position.Bias; -import javax.swing.text.StyleConstants; -import javax.swing.text.View; -import org.netbeans.api.editor.fold.Fold; -import org.netbeans.api.editor.settings.FontColorNames; -import org.netbeans.api.editor.settings.FontColorSettings; -import org.netbeans.modules.editor.lib2.view.EditorView; -import org.netbeans.modules.editor.lib2.view.ViewRenderContext; -import org.netbeans.modules.editor.lib2.view.ViewUtils; - -/** - * View with highlights. This is the most used view. - * - * @author Miloslav Metelka - */ - -public class FoldView extends EditorView { - - // -J-Dorg.netbeans.modules.editor.lib2.view.HighlightsView.level=FINE - private static final Logger LOG = Logger.getLogger(FoldView.class.getName()); - - /** - * Extra space added to each side of description text of a fold view. - */ - private static final float EXTRA_MARGIN_WIDTH = 3; - - /** Raw end offset of this view. */ - private int rawEndOffset; // 24-super + 4 = 28 bytes - - /** Length of text occupied by this view. */ - private int length; // 28 + 4 = 32 bytes - - private final JTextComponent textComponent; // 32 + 4 = 36 bytes - - private final Fold fold; // 36 + 4 = 40 bytes - - private TextLayout collapsedTextLayout; // 40 + 4 = 44 bytes - - private AttributeSet foldingColors; - - public FoldView(JTextComponent textComponent, Fold fold, FontColorSettings colorSettings) { - super(null); - int offset = fold.getStartOffset(); - int len = fold.getEndOffset() - offset; - assert (len > 0) : "length=" + len + " <= 0"; // NOI18N - this.length = len; - this.textComponent = textComponent; - this.fold = fold; - this.foldingColors = colorSettings.getFontColors(FontColorNames.CODE_FOLDING_COLORING); - } - - @Override - public float getPreferredSpan(int axis) { - TextLayout textLayout = getTextLayout(); - if (textLayout == null) { - return 0f; - } - String desc = fold.getDescription(); // For empty desc a single-space text layout is returned - if (axis == View.X_AXIS) { - return ((desc.length() > 0) ? textLayout.getAdvance() : 0) - + (2 * EXTRA_MARGIN_WIDTH); - } else { - EditorView.Parent parent = (EditorView.Parent) getParent(); - return (parent != null) ? parent.getViewRenderContext().getDefaultRowHeight() : 0f; - } - } - - @Override - public int getRawEndOffset() { - return rawEndOffset; - } - - @Override - public void setRawEndOffset(int rawOffset) { - this.rawEndOffset = rawOffset; - } - - @Override - public int getLength() { - return length; - } - - @Override - public int getStartOffset() { - return getEndOffset() - getLength(); - } - - @Override - public int getEndOffset() { - EditorView.Parent parent = (EditorView.Parent) getParent(); - return (parent != null) ? parent.getViewEndOffset(rawEndOffset) : rawEndOffset; - } - - @Override - public Document getDocument() { - View parent = getParent(); - return (parent != null) ? parent.getDocument() : null; - } - - @Override - public AttributeSet getAttributes() { - return null; - } - - private TextLayout getTextLayout() { - if (collapsedTextLayout == null) { - EditorView.Parent parent = (EditorView.Parent) getParent(); - ViewRenderContext context = parent.getViewRenderContext(); - FontRenderContext frc = context.getFontRenderContext(); - assert (frc != null) : "Null FontRenderContext"; // NOI18N - Font font = context.getRenderFont(textComponent.getFont()); - String text = fold.getDescription(); - if (text.length() == 0) { - text = " "; // Use single space (mainly for height measurement etc. - } - collapsedTextLayout = new TextLayout(text, font, frc); - } - return collapsedTextLayout; - } - - @Override - public Shape modelToViewChecked(int offset, Shape alloc, Position.Bias bias) { -// TextLayout textLayout = getTextLayout(); -// if (textLayout == null) { -// return alloc; // Leave given bounds -// } -// Rectangle2D.Double bounds = ViewUtils.shape2Bounds(alloc); -// return bounds; - return alloc; - } - - @Override - public int viewToModelChecked(double x, double y, Shape alloc, Position.Bias[] biasReturn) { - int startOffset = getStartOffset(); - return startOffset; - } - - static TextHitInfo x2RelOffset(TextLayout textLayout, float x) { - TextHitInfo hit; - x -= EXTRA_MARGIN_WIDTH; - if (x >= textLayout.getAdvance()) { - hit = TextHitInfo.trailing(textLayout.getCharacterCount()); - } else { - hit = textLayout.hitTestChar(x, 0); // What about backward bias -> with higher offsets it may go back visually - } - return hit; - - } - - @Override - public int getNextVisualPositionFromChecked(int offset, Bias bias, Shape alloc, int direction, Bias[] biasRet) { - int startOffset = getStartOffset(); - int retOffset = -1; - switch (direction) { - case WEST: - if (offset == -1) { - retOffset = startOffset; - } else { - retOffset = -1; - } - break; - - case EAST: - if (offset == -1) { - retOffset = startOffset; - } else { - retOffset = -1; - } - break; - - case NORTH: - case SOUTH: - break; - default: - throw new IllegalArgumentException("Bad direction: " + direction); - } - return retOffset; - } - - @Override - public JComponent getToolTip(double x, double y, Shape allocation) { - Container container = getContainer(); - if (container instanceof JEditorPane) { - JEditorPane editorPane = (JEditorPane) getContainer(); - JEditorPane tooltipPane = new JEditorPane(); - EditorKit kit = editorPane.getEditorKit(); - Document doc = getDocument(); - if (kit != null && doc != null) { - Element lineRootElement = doc.getDefaultRootElement(); - tooltipPane.putClientProperty(FoldViewFactory.VIEW_FOLDS_EXPANDED_PROPERTY, true); - try { - // Start-offset of the fold => line start => position - int lineIndex = lineRootElement.getElementIndex(fold.getStartOffset()); - Position pos = doc.createPosition( - lineRootElement.getElement(lineIndex).getStartOffset()); - // DocumentView.START_POSITION_PROPERTY - tooltipPane.putClientProperty("document-view-start-position", pos); - // End-offset of the fold => line end => position - lineIndex = lineRootElement.getElementIndex(fold.getEndOffset()); - pos = doc.createPosition(lineRootElement.getElement(lineIndex).getEndOffset()); - // DocumentView.END_POSITION_PROPERTY - tooltipPane.putClientProperty("document-view-end-position", pos); - tooltipPane.putClientProperty("document-view-accurate-span", true); - // Set the same kit and document - tooltipPane.setEditorKit(kit); - tooltipPane.setDocument(doc); - tooltipPane.setEditable(false); - return new FoldToolTip(editorPane, tooltipPane, getForegroundColor()); - } catch (BadLocationException e) { - // => return null - } - } - } - return null; - } - - private Color getForegroundColor() { - if (foldingColors == null) { - return textComponent.getForeground(); - } - Object bgColorObj = foldingColors.getAttribute(StyleConstants.Foreground); - if (bgColorObj instanceof Color) { - return (Color)bgColorObj; - } else { - return textComponent.getForeground(); - } - } - - private Color getBackgroundColor() { - if (foldingColors == null) { - return textComponent.getBackground(); - } - Object bgColorObj = foldingColors.getAttribute(StyleConstants.Background); - if (bgColorObj instanceof Color) { - return (Color)bgColorObj; - } else { - return textComponent.getBackground(); - } - } - - @Override - public void paint(Graphics2D g, Shape alloc, Rectangle clipBounds) { - Rectangle2D.Double allocBounds = ViewUtils.shape2Bounds(alloc); - if (allocBounds.intersects(clipBounds)) { - Font origFont = g.getFont(); - Color origColor = g.getColor(); - Color origBkColor = g.getBackground(); - Shape origClip = g.getClip(); - try { - // Leave component font - g.setColor(getForegroundColor()); - g.setBackground(getBackgroundColor()); - - int xInt = (int) allocBounds.getX(); - int yInt = (int) allocBounds.getY(); - int endXInt = (int) (allocBounds.getX() + allocBounds.getWidth() - 1); - int endYInt = (int) (allocBounds.getY() + allocBounds.getHeight() - 1); - g.drawRect(xInt, yInt, endXInt - xInt, endYInt - yInt); - g.clearRect(xInt + 1, yInt + 1, endXInt - xInt - 1, endYInt - yInt - 1); - g.clip(alloc); - TextLayout textLayout = getTextLayout(); - if (textLayout != null) { - EditorView.Parent parent = (EditorView.Parent) getParent(); - float ascent = parent.getViewRenderContext().getDefaultAscent(); - String desc = fold.getDescription(); // For empty desc a single-space text layout is returned - float x = (float) (allocBounds.getX() + EXTRA_MARGIN_WIDTH); - float y = (float) allocBounds.getY(); - if (desc.length() > 0) { - - textLayout.draw(g, x, y + ascent); - } - } - } finally { - g.setClip(origClip); - g.setBackground(origBkColor); - g.setColor(origColor); - g.setFont(origFont); - } - } - } - - @Override - protected String getDumpName() { - return "FV"; - } - - @Override - public String toString() { - return appendViewInfo(new StringBuilder(200), 0, "", -1).toString(); - } - -} diff --git a/editor.fold/src/org/netbeans/modules/editor/fold/FoldViewFactory.java b/editor.fold/src/org/netbeans/modules/editor/fold/FoldViewFactory.java deleted file mode 100644 --- a/editor.fold/src/org/netbeans/modules/editor/fold/FoldViewFactory.java +++ /dev/null @@ -1,238 +0,0 @@ -/* - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. - * - * Copyright 1997-2010 Oracle and/or its affiliates. All rights reserved. - * - * Oracle and Java are registered trademarks of Oracle and/or its affiliates. - * Other names may be trademarks of their respective owners. - * - * The contents of this file are subject to the terms of either the GNU - * General Public License Version 2 only ("GPL") or the Common - * Development and Distribution License("CDDL") (collectively, the - * "License"). You may not use this file except in compliance with the - * License. You can obtain a copy of the License at - * http://www.netbeans.org/cddl-gplv2.html - * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the - * specific language governing permissions and limitations under the - * License. When distributing the software, include this License Header - * Notice in each file and include the License file at - * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the GPL Version 2 section of the License file that - * accompanied this code. If applicable, add the following below the - * License Header, with the fields enclosed by brackets [] replaced by - * your own identifying information: - * "Portions Copyrighted [year] [name of copyright owner]" - * - * Contributor(s): - * - * The Original Software is NetBeans. The Initial Developer of the Original - * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun - * Microsystems, Inc. All Rights Reserved. - * - * If you wish your version of this file to be governed by only the CDDL - * or only the GPL Version 2, indicate your decision by adding - * "[Contributor] elects to include this software in this distribution - * under the [CDDL or GPL Version 2] license." If you do not indicate a - * single choice of license, a recipient has the option to distribute - * your version of this file under either the CDDL, the GPL Version 2 or - * to extend the choice of license to its licensees as provided above. - * However, if you add GPL Version 2 code and therefore, elected the GPL - * Version 2 license, then the option applies only if the new code is - * made subject to such option by the copyright holder. - */ - -package org.netbeans.modules.editor.fold; - -import java.util.Iterator; -import java.util.logging.Level; -import java.util.logging.Logger; -import javax.swing.text.View; -import org.netbeans.api.editor.fold.Fold; -import org.netbeans.api.editor.fold.FoldHierarchy; -import org.netbeans.api.editor.fold.FoldHierarchyEvent; -import org.netbeans.api.editor.fold.FoldHierarchyListener; -import org.netbeans.api.editor.fold.FoldUtilities; -import org.netbeans.api.editor.mimelookup.MimeLookup; -import org.netbeans.api.editor.settings.FontColorSettings; -import org.netbeans.lib.editor.util.swing.DocumentUtilities; -import org.netbeans.modules.editor.lib2.view.EditorView; -import org.netbeans.modules.editor.lib2.view.EditorViewFactory; -import org.netbeans.modules.editor.lib2.view.EditorViewFactoryChange; -import org.netbeans.modules.editor.lib2.view.ViewUtils; -import org.openide.util.Lookup; -import org.openide.util.LookupEvent; -import org.openide.util.LookupListener; -import org.openide.util.WeakListeners; - -/** - * View factory creating views for collapsed folds. - * - * @author Miloslav Metelka - */ - -@SuppressWarnings("ClassWithMultipleLoggers") -public final class FoldViewFactory extends EditorViewFactory implements FoldHierarchyListener, LookupListener { - - /** - * Component's client property which can be set to view folds expanded for tooltip fold preview. - */ - static final String VIEW_FOLDS_EXPANDED_PROPERTY = "view-folds-expanded"; // NOI18N - - // -J-Dorg.netbeans.editor.view.change.level=FINE - static final Logger CHANGE_LOG = Logger.getLogger("org.netbeans.editor.view.change"); - - // -J-Dorg.netbeans.modules.editor.fold.FoldViewFactory.level=FINE - private static final Logger LOG = Logger.getLogger(FoldViewFactory.class.getName()); - - static void register() { - EditorViewFactory.registerFactory(new FoldFactory()); - } - - private FoldHierarchy foldHierarchy; - - private boolean foldHierarchyLocked; - - private Fold fold; - - private int foldStartOffset; - - private Iterator collapsedFoldIterator; - - private boolean viewFoldsExpanded; - - /** - * Composite Color settings from MIME lookup - */ - private FontColorSettings colorSettings; - - /** - * Lookup results for color settings, being listened for changes. - */ - private Lookup.Result colorSource; - - public FoldViewFactory(View documentView) { - super(documentView); - foldHierarchy = FoldHierarchy.get(textComponent()); - foldHierarchy.addFoldHierarchyListener(this); - viewFoldsExpanded = Boolean.TRUE.equals(textComponent().getClientProperty(VIEW_FOLDS_EXPANDED_PROPERTY)); - - String mime = DocumentUtilities.getMimeType(document()); - - Lookup lkp = MimeLookup.getLookup(mime); - colorSource = lkp.lookupResult(FontColorSettings.class); - colorSource.addLookupListener(WeakListeners.create(LookupListener.class, this, colorSource)); - colorSettings = (FontColorSettings)colorSource.allInstances().iterator().next(); - } - - @Override - public void resultChanged(LookupEvent ev) { - refreshColors(); - } - - private void refreshColors() { - colorSettings = (FontColorSettings)colorSource.allInstances().iterator().next(); - document().render(new Runnable() { - @Override - public void run() { - int end = document().getLength(); - fireEvent(EditorViewFactoryChange.createList(0, end, EditorViewFactoryChange.Type.CHARACTER_CHANGE)); - } - }); - } - - @Override - public void restart(int startOffset, int endOffset, boolean createViews) { - foldHierarchy.lock(); // this.finish() always called in try-finally - foldHierarchyLocked = true; - @SuppressWarnings("unchecked") - Iterator it = FoldUtilities.collapsedFoldIterator(foldHierarchy, startOffset, Integer.MAX_VALUE); - collapsedFoldIterator = it; - foldStartOffset = -1; // Make a next call to updateFold() to fetch a fold - } - - private void updateFold(int offset) { - if (foldStartOffset < offset) { - while (collapsedFoldIterator.hasNext()) { - fold = collapsedFoldIterator.next(); - foldStartOffset = fold.getStartOffset(); - if (foldStartOffset >= offset) { - return; - } - } - fold = null; - foldStartOffset = Integer.MAX_VALUE; - } - } - - @Override - public int nextViewStartOffset(int offset) { - if (!viewFoldsExpanded) { - updateFold(offset); - return foldStartOffset; - } - return Integer.MAX_VALUE; - } - - @Override - public EditorView createView(int startOffset, int limitOffset, boolean forcedLimit, - EditorView origView, int nextOrigViewOffset) { - assert (startOffset == foldStartOffset) : "startOffset=" + startOffset + " != foldStartOffset=" + foldStartOffset; // NOI18N - if (fold.getEndOffset() <= limitOffset || !forcedLimit) { - return new FoldView(textComponent(), fold, colorSettings); - } else { - return null; - } - } - - @Override - public int viewEndOffset(int startOffset, int limitOffset, boolean forcedLimit) { - int foldEndOffset = fold.getEndOffset(); - if (foldEndOffset <= limitOffset) { - return foldEndOffset; - } else { - return -1; - } - } - - @Override - public void continueCreation(int startOffset, int endOffset) { - } - - @Override - public void finishCreation() { - fold = null; - collapsedFoldIterator = null; - if (foldHierarchyLocked) { - foldHierarchy.unlock(); - } - } - - @Override - public void foldHierarchyChanged(FoldHierarchyEvent evt) { - // For fold state changes use a higher priority - int startOffset = evt.getAffectedStartOffset(); - int endOffset = evt.getAffectedEndOffset(); - if (CHANGE_LOG.isLoggable(Level.FINE)) { - ViewUtils.log(CHANGE_LOG, "CHANGE in FoldViewFactory: <" + // NOI18N - startOffset + "," + endOffset + ">\n"); // NOI18N - } - fireEvent(EditorViewFactoryChange.createList(startOffset, endOffset, - EditorViewFactoryChange.Type.PARAGRAPH_CHANGE)); - } - - public static final class FoldFactory implements EditorViewFactory.Factory { - - @Override - public EditorViewFactory createEditorViewFactory(View documentView) { - return new FoldViewFactory(documentView); - } - - @Override - public int weight() { - return 100; - } - - } - -} diff --git a/editor.fold/src/org/netbeans/modules/editor/fold/FoldingEditorSupport.java b/editor.fold/src/org/netbeans/modules/editor/fold/FoldingEditorSupport.java new file mode 100644 --- /dev/null +++ b/editor.fold/src/org/netbeans/modules/editor/fold/FoldingEditorSupport.java @@ -0,0 +1,170 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2013 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + * + * Contributor(s): + * + * Portions Copyrighted 2013 Sun Microsystems, Inc. + */ +package org.netbeans.modules.editor.fold; + +import java.awt.Rectangle; +import java.util.concurrent.Callable; +import java.util.logging.Logger; +import javax.swing.SwingUtilities; +import javax.swing.text.Document; +import javax.swing.text.JTextComponent; +import org.netbeans.api.editor.fold.Fold; +import org.netbeans.api.editor.fold.FoldHierarchy; +import org.netbeans.api.editor.fold.FoldHierarchyEvent; +import org.netbeans.api.editor.fold.FoldHierarchyListener; +import org.netbeans.api.editor.fold.FoldStateChange; +import org.netbeans.api.editor.fold.FoldUtilities; +import org.netbeans.editor.BaseCaret; + +/** + * Provides adjustments to functions of editor component + * based on folding operations. + * This code was originally part of editor.lib, in BaseCaret class. + * + * @author sdedic + */ +class FoldingEditorSupport implements Callable, FoldHierarchyListener { + private static final Logger LOG = Logger.getLogger(FoldingEditorSupport.class.getName()); + + /** + * Component where the folding takes place + */ + private final JTextComponent component; + + /** + * Fold hierarchy + */ + private final FoldHierarchy foldHierarchy; + + FoldingEditorSupport(FoldHierarchy h, JTextComponent component) { + this.component = component; + this.foldHierarchy = h; + component.putClientProperty("org.netbeans.api.fold.expander", this); + foldHierarchy.addFoldHierarchyListener(this); + } + + /** + * Callable that autoexpands place with caret. + * The Callable is called from the BaseCaret. Since + * @return + */ + public Boolean call() { + final Document doc = component.getDocument(); + final Boolean[] res = new Boolean[] { false }; + doc.render(new Runnable() { + public void run() { + foldHierarchy.lock(); + try { + int offset = component.getCaret().getDot(); + Fold f = FoldUtilities.findCollapsedFold(foldHierarchy, offset, offset); + if (f != null) { + foldHierarchy.expand(f); + res[0] = true; + } + } finally { + foldHierarchy.unlock(); + } + } + }); + return res[0]; + } + + public @Override void foldHierarchyChanged(FoldHierarchyEvent evt) { + if (!(component.getCaret() instanceof BaseCaret)) { + return; + } + int caretOffset = component.getCaret().getDot(); + final int addedFoldCnt = evt.getAddedFoldCount(); + final boolean scrollToView; + LOG.finest("Received fold hierarchy change"); + if (addedFoldCnt > 0) { + FoldHierarchy hierarchy = (FoldHierarchy) evt.getSource(); + Fold collapsed = null; + boolean wasExpanded = false; + while ((collapsed = FoldUtilities.findCollapsedFold(hierarchy, caretOffset, caretOffset)) != null && collapsed.getStartOffset() < caretOffset && + collapsed.getEndOffset() > caretOffset) { + hierarchy.expand(collapsed); + wasExpanded = true; + } + // prevent unneeded scrolling; the user may have scrolled out using mouse already + // so scroll only if the added fold may affect Y axis. Actually it's unclear why + // we should reveal the current position on fold events except when caret is positioned in now-collapsed fold + scrollToView = wasExpanded; + } else { + int startOffset = Integer.MAX_VALUE; + // Set the caret's offset to the end of just collapsed fold if necessary + if (evt.getAffectedStartOffset() <= caretOffset && evt.getAffectedEndOffset() >= caretOffset) { + for (int i = 0; i < evt.getFoldStateChangeCount(); i++) { + FoldStateChange change = evt.getFoldStateChange(i); + if (change.isCollapsedChanged()) { + Fold fold = change.getFold(); + if (fold.isCollapsed() && fold.getStartOffset() <= caretOffset && fold.getEndOffset() >= caretOffset) { + if (fold.getStartOffset() < startOffset) { + startOffset = fold.getStartOffset(); + } + } + } + } + if (startOffset != Integer.MAX_VALUE) { + ((BaseCaret)component.getCaret()).setDot(startOffset, false); + } + } + scrollToView = false; + } + // Update caret's visual position + // Post the caret update asynchronously since the fold hierarchy is updated before + // the view hierarchy and the views so the dispatchUpdate() could be picking obsolete + // view information. + if (addedFoldCnt > 1 || scrollToView) { + SwingUtilities.invokeLater(new Runnable() { + public @Override void run() { + LOG.finest("Updating after fold hierarchy change"); + if (component == null) { + return; + } + ((BaseCaret)component.getCaret()).refresh(addedFoldCnt > 1 && !scrollToView); + } + }); + } + } + +} diff --git a/editor.fold/src/org/netbeans/modules/editor/fold/JavadocReader.java b/editor.fold/src/org/netbeans/modules/editor/fold/JavadocReader.java new file mode 100644 --- /dev/null +++ b/editor.fold/src/org/netbeans/modules/editor/fold/JavadocReader.java @@ -0,0 +1,148 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2013 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + * + * Contributor(s): + * + * Portions Copyrighted 2013 Sun Microsystems, Inc. + */ +package org.netbeans.modules.editor.fold; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.swing.text.BadLocationException; +import javax.swing.text.Document; +import org.netbeans.api.editor.fold.Fold; +import org.netbeans.api.editor.fold.FoldHierarchy; +import org.netbeans.api.editor.fold.FoldTemplate; +import org.netbeans.editor.BaseDocument; +import org.netbeans.editor.Utilities; +import org.netbeans.lib.editor.util.CharSequenceUtilities; +import org.netbeans.lib.editor.util.swing.DocumentUtilities; +import org.netbeans.spi.editor.fold.ContentReader; + + +/** + * The class will read the contents of the document starting after the fold's guarded start. + * It will produce text found on the first non-blank line fully contained within the fold. During + * the search, the Readed ignores leading whitespaces on a line followed by 'lineStartMarker' string. + *

+ * For example, if lineStartMarker is set to '*', it will ignore stars at the beginning of the Javadoc, + * so it will produce the overwiew sentence, although it typically starts at the 2nd javadoc line. If + * the 'lineStartMarker' is set to "#", it will produce 1st line of a consecutive line comment block. + *

+ * The Reader will stop reading the content on line starting with a 'stopPattern'. For javadoc, lines + * that start with @tag will stop the reading. + *

+ * So, for Javadocs, the configuration will look like: + * + *

+ * ContentReader rd = new JavadocReader("*", "\.", "@");
+ * 
+ * + * + * + */ +public final class JavadocReader implements ContentReader { + /** + * The marker, which may (should!) be present at the start of the line + */ + private final String lineStartMarker; + + /** + * Ignore contents after this pattern + */ + private final Pattern stopPattern; + + private final Pattern termPattern; + + private final String prefix; + + public JavadocReader(String lineStartMarker, String terminator, String stopPattern, String prefix) { + this.lineStartMarker = lineStartMarker; + this.termPattern = terminator != null ? Pattern.compile(terminator) : null; + this.stopPattern = stopPattern != null ? Pattern.compile(stopPattern, Pattern.CASE_INSENSITIVE) : null; + this.prefix = prefix == null ? " " : prefix; // NOI18N + } + + @Override + public CharSequence read(Document d, Fold f, FoldTemplate ft) throws BadLocationException { + int contentStart = f.getGuardedStart(); + int contentEnd = f.getGuardedEnd(); + BaseDocument bd = (BaseDocument)d; + + int rowStart = contentStart; + + while (rowStart < contentEnd) { + int nextRow = Utilities.getRowStart(bd, rowStart, 1); + if (nextRow == -1 || nextRow > contentEnd) { + nextRow = contentEnd; + } + int nonWhite = Utilities.getFirstNonWhiteFwd(bd, rowStart, nextRow); + // check if the nonwhite content matches the lineStartMarker + if (nonWhite != -1 && + lineStartMarker != null && + CharSequenceUtilities.textEquals(DocumentUtilities.getText(d, nonWhite, lineStartMarker.length()), lineStartMarker)) { + nonWhite = Utilities.getFirstNonWhiteFwd(bd, nonWhite + lineStartMarker.length(), nextRow); + } + if (nonWhite != -1) { + // found a non-whitespace + int endIndex = Utilities.getFirstNonWhiteBwd(bd, nextRow, nonWhite); + if (endIndex < nextRow) { + endIndex++; + } + CharSequence ret = DocumentUtilities.getText(d, nonWhite, endIndex - nonWhite); + if (stopPattern != null && stopPattern.matcher(ret).lookingAt()) { + return null; + } + int idx = -1; + if (termPattern != null) { + Matcher m = termPattern.matcher(ret); + if (m.find()) { + idx = m.start(); + } + } + if (idx > -1) { + return prefix + DocumentUtilities.getText(d, nonWhite, idx); + } else { + return prefix + ret; + } + } + rowStart = nextRow; + } + return null; + } +} diff --git a/editor.fold/src/org/netbeans/modules/editor/fold/LegacySettingsSync.java b/editor.fold/src/org/netbeans/modules/editor/fold/LegacySettingsSync.java new file mode 100644 --- /dev/null +++ b/editor.fold/src/org/netbeans/modules/editor/fold/LegacySettingsSync.java @@ -0,0 +1,161 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2013 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + * + * Contributor(s): + * + * Portions Copyrighted 2013 Sun Microsystems, Inc. + */ +package org.netbeans.modules.editor.fold; + +import java.lang.ref.Reference; +import java.lang.ref.WeakReference; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.prefs.PreferenceChangeEvent; +import java.util.prefs.PreferenceChangeListener; +import java.util.prefs.Preferences; +import org.netbeans.api.editor.fold.FoldType; +import org.netbeans.api.editor.fold.FoldUtilities; +import org.netbeans.api.editor.mimelookup.MimeLookup; +import org.netbeans.api.editor.mimelookup.MimePath; +import org.netbeans.modules.editor.settings.storage.api.OverridePreferences; + +/** + * The class is responsible for syncing legacy setting keys in the + * default ("") mimetype. It is also responsible for erasing + * folds from a MIME, if the setting for 'override' is not set. This will + * gradually clean up migrated settings. + * + * @author sdedic + */ +class LegacySettingsSync implements PreferenceChangeListener { + private static LegacySettingsSync INSTANCE; + + private Reference defaultMimePrefs; + + synchronized static LegacySettingsSync get() { + if (INSTANCE == null) { + INSTANCE = new LegacySettingsSync(); + } + return INSTANCE; + } + + synchronized Preferences processMime(String mime) { + Preferences prefs = MimeLookup.getLookup(mime).lookup(Preferences.class); + if ("".equals(mime)) { // NOI18N + Preferences p = defaultMimePrefs == null ? null : defaultMimePrefs.get(); + if (p == prefs) { + return prefs; + } else if (p != null) { + p.removePreferenceChangeListener(this); + } + // sync the default values for legacy code + syncKey(FoldType.MEMBER.code(), prefs); + syncKey(FoldType.NESTED.code(), prefs); + syncKey(FoldType.DOCUMENTATION.code(), prefs); + defaultMimePrefs = new WeakReference(prefs); + // no weak listener, this instance lives forever, but the defaultMimePrefs + // reference allows the pref to expire. + prefs.addPreferenceChangeListener(this); + } else { + if (!((prefs instanceof OverridePreferences) && ((OverridePreferences)prefs).isOverriden(FoldUtilitiesImpl.PREF_OVERRIDE_DEFAULTS))) { + return prefs; + } + boolean state = prefs.getBoolean(FoldUtilitiesImpl.PREF_OVERRIDE_DEFAULTS, false); + if (!state) { + cleanupPreferences(mime, prefs); + } else { + clonePreferences(mime, prefs); + } + } + return prefs; + } + + @Override + public void preferenceChange(PreferenceChangeEvent evt) { + if (evt.getKey() == null || !evt.getKey().startsWith(FoldUtilitiesImpl.PREF_COLLAPSE_PREFIX)) { + return; + } + String k = evt.getKey().substring(FoldUtilitiesImpl.PREF_COLLAPSE_PREFIX.length()); + syncKey(k, evt.getNode()); + } + + private void syncKey(String k, Preferences pref) { + String l; + if (FoldType.MEMBER.code().equals(k)) { + l = "method"; // NOI18N + } else if (FoldType.NESTED.code().equals(k)) { + l = "innerclass"; // NOI18N + } else if (FoldType.DOCUMENTATION.code().equals(k)) { + l = "javadoc"; // NOI18N + } else { + return; + } + String syncKey = FoldUtilitiesImpl.PREF_COLLAPSE_PREFIX + l; + pref.putBoolean(syncKey, pref.getBoolean( + FoldUtilitiesImpl.PREF_COLLAPSE_PREFIX + k, false)); + } + + private void clonePreferences(String mime, Preferences pref) { + Collection types = FoldUtilities.getFoldTypes(mime).values(); + for (FoldType ft : types) { + String key = FoldUtilitiesImpl.PREF_COLLAPSE_PREFIX + ft.code(); + if (!isDefinedLocally(pref, key)) { + boolean val = pref.getBoolean(key, + ft.parent() == null ? false : + pref.getBoolean(FoldUtilitiesImpl.PREF_COLLAPSE_PREFIX + ft.parent().code(), false)); + pref.putBoolean(key, val); + } + } + } + + private boolean isDefinedLocally(Preferences pref, String key) { + return pref instanceof OverridePreferences && + ((OverridePreferences)pref).isOverriden(key); + } + + private void cleanupPreferences(String mime, Preferences pref) { + Collection types = FoldUtilities.getFoldTypes(mime).values(); + for (FoldType ft : types) { + String key = FoldUtilitiesImpl.PREF_COLLAPSE_PREFIX + ft.code(); + if (isDefinedLocally(pref, key)) { + pref.remove(key); + } + } + } +} diff --git a/editor.fold/src/org/netbeans/modules/editor/fold/ui/ActionFactory.java b/editor.fold/src/org/netbeans/modules/editor/fold/ui/ActionFactory.java new file mode 100644 --- /dev/null +++ b/editor.fold/src/org/netbeans/modules/editor/fold/ui/ActionFactory.java @@ -0,0 +1,261 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2013 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + * + * Contributor(s): + * + * Portions Copyrighted 2013 Sun Microsystems, Inc. + */ +package org.netbeans.modules.editor.fold.ui; + +import java.awt.event.ActionEvent; +import javax.swing.text.BadLocationException; +import javax.swing.text.Document; +import javax.swing.text.JTextComponent; +import org.netbeans.api.editor.EditorActionRegistration; +import org.netbeans.api.editor.fold.Fold; +import org.netbeans.api.editor.fold.FoldHierarchy; +import org.netbeans.api.editor.fold.FoldUtilities; +import org.netbeans.editor.BaseAction; +import org.netbeans.editor.BaseKit; + +/** + * Factory for editor code folding actions. + * Migrated from editor.lib module + * + * @author sdedic + */ +final class ActionFactory { + + /** Returns the fold that should be collapsed/expanded in the caret row + * @param hierarchy hierarchy under which all folds should be collapsed/expanded. + * @param dot caret position offset + * @param lineStart offset of the start of line + * @param lineEnd offset of the end of line + * @return the fold that meet common criteria in accordance with the caret position + */ + private static Fold getLineFold(FoldHierarchy hierarchy, int dot, int lineStart, int lineEnd){ + Fold caretOffsetFold = FoldUtilities.findOffsetFold(hierarchy, dot); + + // beginning searching from the lineStart + Fold fold = FoldUtilities.findNearestFold(hierarchy, lineStart); + + while (fold!=null && + (fold.getEndOffset()<=dot || // find next available fold if the 'fold' is one-line + // or it has children and the caret is in the fold body + // i.e. class A{ |public void method foo(){}} + (!fold.isCollapsed() && fold.getFoldCount() > 0 && fold.getStartOffset()+10) ? fold.getStartOffset()+1 : fold.getEndOffset()); + if (nextFold!=null && nextFold.getStartOffset()lineEnd) { + + // in the case: + // class A{ + // } | + // try to find an offset fold on the offset of the line beginning + if (caretOffsetFold == null){ + caretOffsetFold = FoldUtilities.findOffsetFold(hierarchy, lineStart); + } + + return caretOffsetFold; + } + + // no fold at offset found, in this case return the fold + if (caretOffsetFold == null) return fold; + + // skip possible inner class members validating if the innerclass fold is collapsed + if (caretOffsetFold.isCollapsed()) return caretOffsetFold; + + // in the case: + // class A{ + // public vo|id foo(){} } + // 'fold' (in this case fold of the method foo) will be returned + if ( caretOffsetFold.getEndOffset()>fold.getEndOffset() && + fold.getEndOffset()>dot){ + return fold; + } + + // class A{ + // |} public void method foo(){} + // inner class fold will be returned + if (fold.getStartOffset()>caretOffsetFold.getEndOffset()) return caretOffsetFold; + + // class A{ + // public void foo(){} |} + // returning innerclass fold + if (fold.getEndOffset() dot || foldRowEnd < dot) return false; // it's not fold encapsulating dot + return true; + } + + + public void actionPerformed(ActionEvent evt, final JTextComponent target) { + Document doc = target.getDocument(); + doc.render(new Runnable() { + @Override + public void run() { + FoldHierarchy hierarchy = FoldHierarchy.get(target); + int dot = target.getCaret().getDot(); + hierarchy.lock(); + try{ + try{ + int rowStart = javax.swing.text.Utilities.getRowStart(target, dot); + int rowEnd = javax.swing.text.Utilities.getRowEnd(target, dot); + Fold fold = FoldUtilities.findNearestFold(hierarchy, rowStart); + fold = getLineFold(hierarchy, dot, rowStart, rowEnd); + if (fold==null){ + return; // no success + } + // ensure we' got the right fold + if (dotInFoldArea(target, fold, dot)){ + hierarchy.collapse(fold); + } + }catch(BadLocationException ble){ + ble.printStackTrace(); + } + }finally { + hierarchy.unlock(); + } + } + }); + } + } + + /** Expand a fold. Depends on the current caret position. */ + @EditorActionRegistration(name = BaseKit.expandFoldAction, + menuText = "#" + BaseKit.expandFoldAction + "_menu_text") + public static class ExpandFold extends LocalBaseAction { + public ExpandFold(){ + } + + public void actionPerformed(ActionEvent evt, final JTextComponent target) { + Document doc = target.getDocument(); + doc.render(new Runnable() { + @Override + public void run() { + FoldHierarchy hierarchy = FoldHierarchy.get(target); + int dot = target.getCaret().getDot(); + hierarchy.lock(); + try { + try { + int rowStart = javax.swing.text.Utilities.getRowStart(target, dot); + int rowEnd = javax.swing.text.Utilities.getRowEnd(target, dot); + Fold fold = getLineFold(hierarchy, dot, rowStart, rowEnd); + if (fold != null) { + hierarchy.expand(fold); + } + } catch (BadLocationException ble) { + ble.printStackTrace(); + } + } finally { + hierarchy.unlock(); + } + } + }); + } + } + + /** Collapse all existing folds in the document. */ + @EditorActionRegistration(name = BaseKit.collapseAllFoldsAction, + menuText = "#" + BaseKit.collapseAllFoldsAction + "_menu_text") + public static class CollapseAllFolds extends LocalBaseAction { + public CollapseAllFolds(){ + } + + public void actionPerformed(ActionEvent evt, JTextComponent target) { + FoldHierarchy hierarchy = FoldHierarchy.get(target); + // Hierarchy locking done in the utility method + FoldUtilities.collapseAll(hierarchy); + } + } + + /** Expand all existing folds in the document. */ + @EditorActionRegistration(name = BaseKit.expandAllFoldsAction, + menuText = "#" + BaseKit.expandAllFoldsAction + "_menu_text") + public static class ExpandAllFolds extends LocalBaseAction { + public ExpandAllFolds(){ + } + + public void actionPerformed(ActionEvent evt, JTextComponent target) { + FoldHierarchy hierarchy = FoldHierarchy.get(target); + // Hierarchy locking done in the utility method + FoldUtilities.expandAll(hierarchy); + } + } + + + private static abstract class LocalBaseAction extends BaseAction { + public LocalBaseAction() { + super(); + } + + @Override + protected Class getShortDescriptionBundleClass() { + return BaseKit.class; + } + + } +} diff --git a/editor.fold/src/org/netbeans/modules/editor/fold/ui/Bundle.properties b/editor.fold/src/org/netbeans/modules/editor/fold/ui/Bundle.properties new file mode 100644 --- /dev/null +++ b/editor.fold/src/org/netbeans/modules/editor/fold/ui/Bundle.properties @@ -0,0 +1,67 @@ +# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. +# +# Copyright 2013 Oracle and/or its affiliates. All rights reserved. +# +# Oracle and Java are registered trademarks of Oracle and/or its affiliates. +# Other names may be trademarks of their respective owners. +# +# The contents of this file are subject to the terms of either the GNU +# General Public License Version 2 only ("GPL") or the Common +# Development and Distribution License("CDDL") (collectively, the +# "License"). You may not use this file except in compliance with the +# License. You can obtain a copy of the License at +# http://www.netbeans.org/cddl-gplv2.html +# or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the +# specific language governing permissions and limitations under the +# License. When distributing the software, include this License Header +# Notice in each file and include the License file at +# nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this +# particular file as subject to the "Classpath" exception as provided +# by Oracle in the GPL Version 2 section of the License file that +# accompanied this code. If applicable, add the following below the +# License Header, with the fields enclosed by brackets [] replaced by +# your own identifying information: +# "Portions Copyrighted [year] [name of copyright owner]" +# +# If you wish your version of this file to be governed by only the CDDL +# or only the GPL Version 2, indicate your decision by adding +# "[Contributor] elects to include this software in this distribution +# under the [CDDL or GPL Version 2] license." If you do not indicate a +# single choice of license, a recipient has the option to distribute +# your version of this file under either the CDDL, the GPL Version 2 or +# to extend the choice of license to its licensees as provided above. +# However, if you add GPL Version 2 code and therefore, elected the GPL +# Version 2 license, then the option applies only if the new code is +# made subject to such option by the copyright holder. +# +# Contributor(s): +# +# Portions Copyrighted 2013 Sun Microsystems, Inc. + +# CodeFoldingSideBar +ACSN_CodeFoldingSideBar=Code folding side bar +ACSD_CodeFoldingSideBar=Code folding side bar shows text folds and allows their collapsing and expanding. + +collapse-all-folds_menu_text=Co&llapse All +collapse-fold_menu_text=&Collapse Fold +expand-all-folds_menu_text=E&xpand All +expand-fold_menu_text=&Expand Fold +collapse-all-folds=Collapse All +collapse-fold=Collapse Fold +expand-all-folds=Expand All +expand-fold=Expand Fold + +CTL_OptionsDisplayName=Folding +KW_Options=Fold +FoldOptionsPanel.langLabel.text=Language: +TITLE_ElementsToCollapse=Collapse by default +FoldOptionsPanel.enableFolds.text=Enable Code Folding +DefaultFoldingOptions.useDefaults.text=Override default settings +DefaultFoldingOptions.collapseContainer.border.title=Collapse by default +FoldOptionsPanel.contentPreview.text=Content preview +FoldOptionsPanel.foldedSummary.text=Show summary +Title_FoldDisplayOptions=Display Options +FMT_ContentPlaceholder_1={0}... +FMT_ContentPlaceholder_2=...{1} +FMT_ContentPlaceholder_3={0} ...{1} +FoldOptionsPanel.useDefaults.text=Use Default Settings diff --git a/editor.fold/src/org/netbeans/modules/editor/fold/ui/CodeFoldingSideBar.java b/editor.fold/src/org/netbeans/modules/editor/fold/ui/CodeFoldingSideBar.java new file mode 100644 --- /dev/null +++ b/editor.fold/src/org/netbeans/modules/editor/fold/ui/CodeFoldingSideBar.java @@ -0,0 +1,1519 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 1997-2010 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * Contributor(s): + * + * The Original Software is NetBeans. The Initial Developer of the Original + * Software is Sun Microsystems, Inc. Portions Copyright 1997-2007 Sun + * Microsystems, Inc. All Rights Reserved. + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + */ + +package org.netbeans.modules.editor.fold.ui; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.FontMetrics; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.Stroke; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.NavigableMap; +import java.util.TreeMap; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.prefs.PreferenceChangeEvent; +import java.util.prefs.PreferenceChangeListener; +import java.util.prefs.Preferences; +import javax.accessibility.Accessible; +import javax.accessibility.AccessibleContext; +import javax.accessibility.AccessibleRole; +import javax.swing.JComponent; +import javax.swing.SwingUtilities; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import javax.swing.text.AbstractDocument; +import javax.swing.text.AttributeSet; +import javax.swing.text.BadLocationException; +import javax.swing.text.Document; +import javax.swing.text.JTextComponent; +import javax.swing.text.View; +import org.netbeans.api.editor.fold.Fold; +import org.netbeans.api.editor.fold.FoldHierarchy; +import org.netbeans.api.editor.fold.FoldHierarchyEvent; +import org.netbeans.api.editor.fold.FoldHierarchyListener; +import org.netbeans.api.editor.fold.FoldUtilities; +import org.netbeans.api.editor.mimelookup.MimeLookup; +import org.netbeans.api.editor.settings.AttributesUtilities; +import org.netbeans.api.editor.settings.FontColorNames; +import org.netbeans.api.editor.settings.FontColorSettings; +import org.netbeans.api.editor.settings.SimpleValueNames; +import org.netbeans.editor.BaseDocument; +import org.netbeans.editor.BaseDocumentEvent; +import org.netbeans.editor.BaseTextUI; +import org.netbeans.editor.Coloring; +import org.netbeans.editor.EditorUI; +import org.netbeans.editor.SideBarFactory; +import org.netbeans.editor.Utilities; +import org.netbeans.modules.editor.fold.ApiPackageAccessor; +import org.netbeans.modules.editor.fold.FoldHierarchyExecution; +import org.netbeans.modules.editor.lib2.EditorPreferencesDefaults; +import org.netbeans.modules.editor.lib2.view.LockedViewHierarchy; +import org.netbeans.modules.editor.lib2.view.ParagraphViewDescriptor; +import org.netbeans.modules.editor.lib2.view.ViewHierarchy; +import org.netbeans.modules.editor.lib2.view.ViewHierarchyEvent; +import org.netbeans.modules.editor.lib2.view.ViewHierarchyListener; +import org.openide.util.Lookup; +import org.openide.util.LookupEvent; +import org.openide.util.LookupListener; +import org.openide.util.NbBundle; +import org.openide.util.WeakListeners; + +/** + * Code Folding Side Bar. Component responsible for drawing folding signs and responding + * on user fold/unfold action. + *

+ * The class was copied/hidden from org.netbeans.editor.CodeFoldingSidebar. If any error is fixed here, + * please fix it as well in {@link org.netbeans.editor.CodeFoldingSidebar} in this module for backward + * compatibility with potential users. + * + * @author Martin Roskanin + */ +public final class CodeFoldingSideBar extends JComponent implements Accessible { + public static final String PROP_SIDEBAR_MARK = "org.netbeans.editor.CodeFoldingSidebar"; // NOI18N + + private static final Logger LOG = Logger.getLogger(CodeFoldingSideBar.class.getName()); + + /** This field should be treated as final. Subclasses are forbidden to change it. + * @deprecated Without any replacement. + */ + protected Color backColor; + /** This field should be treated as final. Subclasses are forbidden to change it. + * @deprecated Without any replacement. + */ + protected Color foreColor; + /** This field should be treated as final. Subclasses are forbidden to change it. + * @deprecated Without any replacement. + */ + protected Font font; + + /** This field should be treated as final. Subclasses are forbidden to change it. */ + protected /*final*/ JTextComponent component; + private volatile AttributeSet attribs; + private Lookup.Result fcsLookupResult; + private final LookupListener fcsTracker = new LookupListener() { + public void resultChanged(LookupEvent ev) { + attribs = null; + SwingUtilities.invokeLater(new Runnable() { + public void run() { + //EMI: This is needed as maybe the DEFAULT_COLORING is changed, the font is different + // and while getMarkSize() is used in paint() and will make the artifacts bigger, + // the component itself will be the same size and it must be changed. + // See http://www.netbeans.org/issues/show_bug.cgi?id=153316 + updatePreferredSize(); + CodeFoldingSideBar.this.repaint(); + } + }); + } + }; + private final Listener listener = new Listener(); + + private boolean enabled = false; + + protected List visibleMarks = new ArrayList(); + + /** + * Mouse moved point, possibly {@code null}. Set from mouse-moved, mouse-entered + * handlers, so that painting will paint this fold in bold. -1, if mouse is not + * in the sidebar region. The value is used to compute highlighted portions of the + * folding outline. + */ + private int mousePoint = -1; + + /** + * if true, the {@link #mousePoint} has been already used to make a PaintInfo active. + * The flag is tested by {@link #traverseForward} and {@link #traverseBackward} after children + * of the current fold are processed and cleared if the {@link #mousePoint} falls to the fold area - + * fields of PaintInfo are set accordingly. + * It's also used to compute (current) mouseBoundary, so mouse movement does not trigger + * refreshes eagerly + */ + private boolean mousePointConsumed; + + /** + * Boundaries of the current area under the mouse. Can be eiher the span of the + * current fold (or part of it), or the span not occupied by any fold. Serves as an optimization + * for mouse handler, which does not trigger painting (refresh) unless mouse + * leaves this region. + */ + private Rectangle mouseBoundary; + + /** + * Y-end of the nearest fold that ends above the {@link #mousePoint}. Undefined if mousePoint is null. + * These two variables are initialized at each level of folds, and help to compute {@link #mouseBoundary} for + * the case the mousePointer is OUTSIDE all children (or outside all folds). + */ + private int lowestAboveMouse = -1; + + /** + * Y-begin of the nearest fold, which starts below the {@link #mousePoint}. Undefined if mousePoint is null + */ + private int topmostBelowMouse = Integer.MAX_VALUE; + + private boolean alreadyPresent; + + /** Paint operations */ + public static final int PAINT_NOOP = 0; + /** + * Normal opening +- marker + */ + public static final int PAINT_MARK = 1; + + /** + * Vertical line - typically at the end of the screen + */ + public static final int PAINT_LINE = 2; + + /** + * End angled line, without a sign + */ + public static final int PAINT_END_MARK = 3; + + /** + * Single-line marker, both start and end + */ + public static final int SINGLE_PAINT_MARK = 4; + + /** + * Marker value for {@link #mousePoint} indicating that mouse is outside the Component. + */ + private static final int NO_MOUSE_POINT = -1; + + /** + * Stroke used to draw inactive (regular) fold outlines. + */ + private static Stroke LINE_DASHED = new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, + 1f, new float[] { 1f, 1f }, 0f); + + /** + * Stroke used to draw outlines for 'active' fold + */ + private static final Stroke LINE_BOLD = new BasicStroke(2, BasicStroke.CAP_ROUND, BasicStroke.JOIN_MITER); + + private final Preferences prefs; + private final PreferenceChangeListener prefsListener = new PreferenceChangeListener() { + public void preferenceChange(PreferenceChangeEvent evt) { + String key = evt == null ? null : evt.getKey(); + if (key == null || SimpleValueNames.CODE_FOLDING_ENABLE.equals(key)) { + updateColors(); + + boolean newEnabled = prefs.getBoolean(SimpleValueNames.CODE_FOLDING_ENABLE, EditorPreferencesDefaults.defaultCodeFoldingEnable); + if (enabled != newEnabled) { + enabled = newEnabled; + updatePreferredSize(); + } + } + } + }; + + private void checkRepaint(ViewHierarchyEvent vhe) { + if (!vhe.isChangeY()) { + // does not obscur sidebar graphics + return; + } + + SwingUtilities.invokeLater(new Runnable() { + public void run() { + updatePreferredSize(); + CodeFoldingSideBar.this.repaint(); + } + }); + } + + /** + * @deprecated Don't use this constructor, it does nothing! + */ + public CodeFoldingSideBar() { + component = null; + prefs = null; + throw new IllegalStateException("Do not use this constructor!"); //NOI18N + } + + public CodeFoldingSideBar(final JTextComponent component){ + super(); + this.component = component; + + // prevent from display CF sidebar twice + if (component.getClientProperty(PROP_SIDEBAR_MARK) == null) { + component.putClientProperty("org.netbeans.editor.CodeFoldingSidebar", Boolean.TRUE); + } else { + alreadyPresent = true; + } + + addMouseListener(listener); + addMouseMotionListener(listener); + + final FoldHierarchy foldHierarchy = FoldHierarchy.get(component); + foldHierarchy.addFoldHierarchyListener(WeakListeners.create(FoldHierarchyListener.class, listener, foldHierarchy)); + + final Document doc = getDocument(); + doc.addDocumentListener(WeakListeners.document(listener, doc)); + setOpaque(true); + + prefs = MimeLookup.getLookup(org.netbeans.lib.editor.util.swing.DocumentUtilities.getMimeType(component)).lookup(Preferences.class); + prefs.addPreferenceChangeListener(WeakListeners.create(PreferenceChangeListener.class, prefsListener, prefs)); + prefsListener.preferenceChange(null); + + ViewHierarchy.get(component).addViewHierarchyListener(new ViewHierarchyListener() { + + @Override + public void viewHierarchyChanged(ViewHierarchyEvent evt) { + checkRepaint(evt); + } + + }); + } + + private boolean hasProviders() { + final FoldHierarchy foldHierarchy = FoldHierarchy.get(component); + FoldHierarchyExecution exec = ApiPackageAccessor.get().foldGetExecution(foldHierarchy); + exec.lock(); + try { + return exec.hasProviders(); + } finally { + exec.unlock(); + } + + } + + private void updatePreferredSize() { + // do not show at all, if there are no providers registered. + if (enabled && hasProviders() && !alreadyPresent) { + setPreferredSize(new Dimension(getColoring().getFont().getSize(), component.getHeight())); + setMaximumSize(new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE)); + }else{ + setPreferredSize(new Dimension(0,0)); + setMaximumSize(new Dimension(0,0)); + } + revalidate(); + } + + private void updateColors() { + Coloring c = getColoring(); + this.backColor = c.getBackColor(); + this.foreColor = c.getForeColor(); + this.font = c.getFont(); + } + + /** + * This method should be treated as final. Subclasses are forbidden to override it. + * @return The background color used for painting this component. + * @deprecated Without any replacement. + */ + protected Color getBackColor() { + if (backColor == null) { + updateColors(); + } + return backColor; + } + + /** + * This method should be treated as final. Subclasses are forbidden to override it. + * @return The foreground color used for painting this component. + * @deprecated Without any replacement. + */ + protected Color getForeColor() { + if (foreColor == null) { + updateColors(); + } + return foreColor; + } + + /** + * This method should be treated as final. Subclasses are forbidden to override it. + * @return The font used for painting this component. + * @deprecated Without any replacement. + */ + protected Font getColoringFont() { + if (font == null) { + updateColors(); + } + return font; + } + + // overriding due to issue #60304 + public @Override void update(Graphics g) { + } + + protected void collectPaintInfos( + View rootView, Fold fold, Map map, int level, int startIndex, int endIndex + ) throws BadLocationException { + //never called + } + + /** + * Adjust lowest/topmost boundaries from the Fold range y1-y2. + * @param y1 + * @param y2 + * @param level + */ + private void setMouseBoundaries(int y1, int y2, int level) { + if (!hasMousePoint() || mousePointConsumed) { + return; + } + int y = mousePoint; + if (y2 < y && lowestAboveMouse < y2) { + LOG.log(Level.FINEST, "lowestAbove at {1}: {0}", new Object[] { y2, level }); + lowestAboveMouse = y2; + } + if (y1 > y && topmostBelowMouse > y1) { + LOG.log(Level.FINEST, "topmostBelow at {1}: {0}", new Object[] { y1, level }); + topmostBelowMouse = y1; + } + } + + /* + * Even collapsed fold MAY contain a continuation line, IF one of folds on the same line is NOT collapsed. Such a fold should + * then visually span multiple lines && be marked as collapsed. + */ + + protected List getPaintInfo(Rectangle clip) throws BadLocationException { + javax.swing.plaf.TextUI textUI = component.getUI(); + if (!(textUI instanceof BaseTextUI)) { + return Collections.emptyList(); + } + BaseTextUI baseTextUI = (BaseTextUI)textUI; + BaseDocument bdoc = Utilities.getDocument(component); + if (bdoc == null) { + return Collections.emptyList(); + } + mousePointConsumed = false; + mouseBoundary = null; + topmostBelowMouse = Integer.MAX_VALUE; + lowestAboveMouse = -1; + bdoc.readLock(); + try { + int startPos = baseTextUI.getPosFromY(clip.y); + int endPos = baseTextUI.viewToModel(component, Short.MAX_VALUE / 2, clip.y + clip.height); + + if (startPos < 0 || endPos < 0) { + // editor window is not properly sized yet; return no infos + return Collections.emptyList(); + } + + // #218282: if the view hierarchy is not yet updated, the Y coordinate may map to an incorrect offset outside + // the document. + int docLen = bdoc.getLength(); + if (startPos >= docLen || endPos > docLen) { + return Collections.emptyList(); + } + + startPos = Utilities.getRowStart(bdoc, startPos); + endPos = Utilities.getRowEnd(bdoc, endPos); + + FoldHierarchy hierarchy = FoldHierarchy.get(component); + hierarchy.lock(); + try { + View rootView = Utilities.getDocumentView(component); + if (rootView != null) { + Object [] arr = getFoldList(hierarchy.getRootFold(), startPos, endPos); + @SuppressWarnings("unchecked") + List foldList = (List) arr[0]; + int idxOfFirstFoldStartingInsideClip = (Integer) arr[1]; + + /* + * Note: + * + * The Map is keyed by Y-VISUAL position of the fold mark, not the textual offset of line start. + * This is because several folds may occupy the same line, while only one + sign is displayed, + * and affect the last fold in the row. + */ + NavigableMap map = new TreeMap(); + // search backwards + for(int i = idxOfFirstFoldStartingInsideClip - 1; i >= 0; i--) { + Fold fold = foldList.get(i); + if (!traverseBackwards(fold, bdoc, baseTextUI, startPos, endPos, 0, map)) { + break; + } + } + + // search forward + for(int i = idxOfFirstFoldStartingInsideClip; i < foldList.size(); i++) { + Fold fold = foldList.get(i); + if (!traverseForward(fold, bdoc, baseTextUI, startPos, endPos, 0, map)) { + break; + } + } + + if (map.isEmpty() && foldList.size() > 0) { + assert foldList.size() == 1; + PaintInfo pi = new PaintInfo(PAINT_LINE, 0, clip.y, clip.height, -1, -1); + mouseBoundary = new Rectangle(0, 0, 0, clip.height); + LOG.log(Level.FINEST, "Mouse boundary for full side line set to: {0}", mouseBoundary); + if (hasMousePoint()) { + pi.markActive(true, true, true); + } + return Collections.singletonList(pi); + } else { + if (mouseBoundary == null) { + mouseBoundary = makeMouseBoundary(clip.y, clip.y + clip.height); + LOG.log(Level.FINEST, "Mouse boundary not set, defaulting to: {0}", mouseBoundary); + } + return new ArrayList(map.values()); + } + } else { + return Collections.emptyList(); + } + } finally { + hierarchy.unlock(); + } + } finally { + bdoc.readUnlock(); + } + } + + /** + * Adds a paint info to the map. If a paintinfo already exists, it merges + * the structures, so the painting process can just follow the instructions. + * + * @param infos + * @param yOffset + * @param nextInfo + */ + private void addPaintInfo(Map infos, int yOffset, PaintInfo nextInfo) { + PaintInfo prevInfo = infos.get(yOffset); + nextInfo.mergeWith(prevInfo); + infos.put(yOffset, nextInfo); + } + + private boolean traverseForward(Fold f, BaseDocument doc, BaseTextUI btui, int lowerBoundary, int upperBoundary,int level, NavigableMap infos) throws BadLocationException { +// System.out.println("~~~ traverseForward<" + lowerBoundary + ", " + upperBoundary +// + ">: fold=<" + f.getStartOffset() + ", " + f.getEndOffset() + "> " +// + (f.getStartOffset() > upperBoundary ? ", f.gSO > uB" : "") +// + ", level=" + level); + + if (f.getStartOffset() > upperBoundary) { + return false; + } + + int lineStartOffset1 = Utilities.getRowStart(doc, f.getStartOffset()); + int lineStartOffset2 = Utilities.getRowStart(doc, f.getEndOffset()); + int y1 = btui.getYFromPos(lineStartOffset1); + int h = btui.getEditorUI().getLineHeight(); + int y2 = btui.getYFromPos(lineStartOffset2); + + // the 'active' flags can be set only after children are processed; highlights + // correspond to the innermost expanded child. + boolean activeMark = false; + boolean activeIn = false; + boolean activeOut = false; + PaintInfo spi; + boolean activated; + + if (y1 == y2) { + // whole fold is on a single line + spi = new PaintInfo(SINGLE_PAINT_MARK, level, y1, h, f.isCollapsed(), lineStartOffset1, lineStartOffset2); + if (activated = isActivated(y1, y1 + h)) { + activeMark = true; + } + addPaintInfo(infos, y1, spi); + } else { + // fold spans multiple lines + spi = new PaintInfo(PAINT_MARK, level, y1, h, f.isCollapsed(), lineStartOffset1, lineStartOffset2); + if (activated = isActivated(y1, y2 + h / 2)) { + activeMark = true; + activeOut = true; + } + addPaintInfo(infos, y1, spi); + } + + setMouseBoundaries(y1, y2 + h / 2, level); + + // Handle end mark after possible inner folds were processed because + // otherwise if there would be two nested folds both ending at the same line + // then the end mark for outer one would be replaced by an end mark for inner one + // (same key in infos map) and the painting code would continue to paint line marking a fold + // until next fold is reached (or end of doc). + PaintInfo epi = null; + if (y1 != y2 && !f.isCollapsed() && f.getEndOffset() <= upperBoundary) { + epi = new PaintInfo(PAINT_END_MARK, level, y2, h, lineStartOffset1, lineStartOffset2); + addPaintInfo(infos, y2, epi); + } + + // save the topmost/lowest information, reset for child processing + int topmost = topmostBelowMouse; + int lowest = lowestAboveMouse; + topmostBelowMouse = y2 + h / 2; + lowestAboveMouse = y1; + + try { + if (!f.isCollapsed()) { + Object [] arr = getFoldList(f, lowerBoundary, upperBoundary); + @SuppressWarnings("unchecked") + List foldList = (List) arr[0]; + int idxOfFirstFoldStartingInsideClip = (Integer) arr[1]; + + // search backwards + for(int i = idxOfFirstFoldStartingInsideClip - 1; i >= 0; i--) { + Fold fold = foldList.get(i); + if (!traverseBackwards(fold, doc, btui, lowerBoundary, upperBoundary, level + 1, infos)) { + break; + } + } + + // search forward + for(int i = idxOfFirstFoldStartingInsideClip; i < foldList.size(); i++) { + Fold fold = foldList.get(i); + if (!traverseForward(fold, doc, btui, lowerBoundary, upperBoundary, level + 1, infos)) { + return false; + } + } + } + if (!mousePointConsumed && activated) { + mousePointConsumed = true; + mouseBoundary = makeMouseBoundary(y1, y2 + h); + LOG.log(Level.FINEST, "Mouse boundary set to: {0}", mouseBoundary); + spi.markActive(activeMark, activeIn, activeOut); + if (epi != null) { + epi.markActive(true, true, false); + } + markDeepChildrenActive(infos, y1, y2, level); + } + } finally { + topmostBelowMouse = topmost; + lowestAboveMouse = lowest; + } + return true; + } + + /** + * Sets outlines of all children to 'active'. Assuming yFrom and yTo are from-to Y-coordinates of the parent + * fold, it finds all nested folds (folds, which are in between yFrom and yTo) and changes their in/out lines + * as active. + * The method returns Y start coordinate of the 1st child found. + * + * @param infos fold infos collected so far + * @param yFrom upper Y-coordinate of the parent fold + * @param yTo lower Y-coordinate of the parent fold + * @param level level of the parent fold + * @return Y-coordinate of the 1st child. + */ + private int markDeepChildrenActive(NavigableMap infos, int yFrom, int yTo, int level) { + int result = Integer.MAX_VALUE; + Map m = infos.subMap(yFrom, yTo); + for (Map.Entry me : m.entrySet()) { + PaintInfo pi = me.getValue(); + int y = pi.getPaintY(); + if (y > yFrom && y < yTo) { + if (LOG.isLoggable(Level.FINEST)) { + LOG.log(Level.FINEST, "Marking chind as active: {0}", pi); + } + pi.markActive(false, true, true); + if (y < result) { + y = result; + } + } + } + return result; + } + + /** + * Returns stroke appropriate for painting (in)active outlines + * @param s the default stroke + * @param active true for active outlines + * @return value of 's' or a Stroke which should be used to paint the outline. + */ + private static Stroke getStroke(Stroke s, boolean active) { + if (active) { + return LINE_BOLD; + } else { + return s; + } + } + + private boolean traverseBackwards(Fold f, BaseDocument doc, BaseTextUI btui, int lowerBoundary, int upperBoundary, int level, NavigableMap infos) throws BadLocationException { +// System.out.println("~~~ traverseBackwards<" + lowerBoundary + ", " + upperBoundary +// + ">: fold=<" + f.getStartOffset() + ", " + f.getEndOffset() + "> " +// + (f.getEndOffset() < lowerBoundary ? ", f.gEO < lB" : "") +// + ", level=" + level); + + if (f.getEndOffset() < lowerBoundary) { + return false; + } + + int lineStartOffset1 = Utilities.getRowStart(doc, f.getStartOffset()); + int lineStartOffset2 = Utilities.getRowStart(doc, f.getEndOffset()); + int h = btui.getEditorUI().getLineHeight(); + + boolean activeMark = false; + boolean activeIn = false; + boolean activeOut = false; + PaintInfo spi = null; + PaintInfo epi = null; + boolean activated = false; + int y1 = 0; + int y2 = 0; + + if (lineStartOffset1 == lineStartOffset2) { + // whole fold is on a single line + y2 = y1 = btui.getYFromPos(lineStartOffset1); + spi = new PaintInfo(SINGLE_PAINT_MARK, level, y1, h, f.isCollapsed(), lineStartOffset1, lineStartOffset1); + if (activated = isActivated(y1, y1 + h)) { + activeMark = true; + } + addPaintInfo(infos, y1, spi); + } else { + y2 = btui.getYFromPos(lineStartOffset2); + // fold spans multiple lines + y1 = btui.getYFromPos(lineStartOffset1); + activated = isActivated(y1, y2 + h / 2); + if (f.getStartOffset() >= upperBoundary) { + spi = new PaintInfo(PAINT_MARK, level, y1, h, f.isCollapsed(), lineStartOffset1, lineStartOffset2); + if (activated) { + activeMark = true; + activeOut = true; + } + addPaintInfo(infos, y1, spi); + } + + if (!f.isCollapsed() && f.getEndOffset() <= upperBoundary) { + activated |= isActivated(y1, y2 + h / 2); + epi = new PaintInfo(PAINT_END_MARK, level, y2, h, lineStartOffset1, lineStartOffset2); + addPaintInfo(infos, y2, epi); + } + } + + setMouseBoundaries(y1, y2 + h / 2, level); + + // save the topmost/lowest information, reset for child processing + int topmost = topmostBelowMouse; + int lowest = lowestAboveMouse; + topmostBelowMouse = y2 + h /2; + lowestAboveMouse = y1; + + try { + if (!f.isCollapsed()) { + Object [] arr = getFoldList(f, lowerBoundary, upperBoundary); + @SuppressWarnings("unchecked") + List foldList = (List) arr[0]; + int idxOfFirstFoldStartingInsideClip = (Integer) arr[1]; + + // search backwards + for(int i = idxOfFirstFoldStartingInsideClip - 1; i >= 0; i--) { + Fold fold = foldList.get(i); + if (!traverseBackwards(fold, doc, btui, lowerBoundary, upperBoundary, level + 1, infos)) { + return false; + } + } + + // search forward + for(int i = idxOfFirstFoldStartingInsideClip; i < foldList.size(); i++) { + Fold fold = foldList.get(i); + if (!traverseForward(fold, doc, btui, lowerBoundary, upperBoundary, level + 1, infos)) { + break; + } + } + } + if (!mousePointConsumed && activated) { + mousePointConsumed = true; + mouseBoundary = makeMouseBoundary(y1, y2 + h); + LOG.log(Level.FINEST, "Mouse boundary set to: {0}", mouseBoundary); + if (spi != null) { + spi.markActive(activeMark, activeIn, activeOut); + } + if (epi != null) { + epi.markActive(true, true, false); + } + int lowestChild = markDeepChildrenActive(infos, y1, y2, level); + if (lowestChild < Integer.MAX_VALUE && lineStartOffset1 < upperBoundary) { + // the fold starts above the screen clip region, and is 'activated'. We need to setup instructions to draw activated line up to the + // 1st child marker. + epi = new PaintInfo(PAINT_LINE, level, y1, y2 - y1, false, lineStartOffset1, lineStartOffset2); + epi.markActive(true, true, false); + addPaintInfo(infos, y1, epi); + } + } + } finally { + topmostBelowMouse = topmost; + lowestAboveMouse = lowest; + } + return true; + } + + private Rectangle makeMouseBoundary(int y1, int y2) { + if (!hasMousePoint()) { + return null; + } + if (topmostBelowMouse < Integer.MAX_VALUE) { + y2 = topmostBelowMouse; + } + if (lowestAboveMouse > -1) { + y1 = lowestAboveMouse; + } + return new Rectangle(0, y1, 0, y2 - y1); + } + + protected EditorUI getEditorUI(){ + return Utilities.getEditorUI(component); + } + + protected Document getDocument(){ + return component.getDocument(); + } + + + private Fold getLastLineFold(FoldHierarchy hierarchy, int rowStart, int rowEnd, boolean shift){ + Fold fold = FoldUtilities.findNearestFold(hierarchy, rowStart); + Fold prevFold = fold; + while (fold != null && fold.getStartOffset()= startY && mouseY <= nextY) { + LOG.log(Level.FINEST, "Starting line clicked, ignoring. MouseY={0}, startY={1}, nextY={2}", + new Object[] { mouseY, startY, nextY }); + return; + } + + startY = textUI.getYFromPos(endOffset); + nextLineOffset = Utilities.getRowStart(bdoc, endOffset, 1); + nextY = textUI.getYFromPos(nextLineOffset); + + if (mouseY >= startY && mouseY <= nextY) { + // the mouse can be positioned above the marker (the fold found above), or + // below it; in that case, the immediate enclosing fold should be used - should be the fold + // that corresponds to the nextLineOffset, if any + int h2 = (startY + nextY) / 2; + if (mouseY >= h2) { + Fold f2 = f; + + f = FoldUtilities.findOffsetFold(hierarchy, nextLineOffset); + if (f == null) { + // fold does not exist for the position below end-of-fold indicator + return; + } + } + + } + + LOG.log(Level.FINEST, "Collapsing fold: {0}", f); + hierarchy.collapse(f); + } finally { + hierarchy.unlock(); + } + } finally { + bdoc.readUnlock(); + } + } + + private void performAction(final Mark mark, final boolean shiftFold) { + Document doc = component.getDocument(); + doc.render(new Runnable() { + @Override + public void run() { + ViewHierarchy vh = ViewHierarchy.get(component); + LockedViewHierarchy lockedVH = vh.lock(); + try { + int pViewIndex = lockedVH.yToParagraphViewIndex(mark.y + mark.size / 2); + if (pViewIndex >= 0) { + ParagraphViewDescriptor pViewDesc = lockedVH.getParagraphViewDescriptor(pViewIndex); + int pViewStartOffset = pViewDesc.getStartOffset(); + int pViewEndOffset = pViewStartOffset + pViewDesc.getLength(); + // Find corresponding fold + FoldHierarchy foldHierarchy = FoldHierarchy.get(component); + foldHierarchy.lock(); + try { + int rowStart = javax.swing.text.Utilities.getRowStart(component, pViewStartOffset); + int rowEnd = javax.swing.text.Utilities.getRowEnd(component, pViewStartOffset); + Fold clickedFold = getLastLineFold(foldHierarchy, rowStart, rowEnd, shiftFold);//FoldUtilities.findNearestFold(foldHierarchy, viewStartOffset); + if (clickedFold != null && clickedFold.getStartOffset() < pViewEndOffset) { + foldHierarchy.toggle(clickedFold); + } + } catch (BadLocationException ble) { + LOG.log(Level.WARNING, null, ble); + } finally { + foldHierarchy.unlock(); + } + } + } finally { + lockedVH.unlock(); + } + } + }); + } + + protected int getMarkSize(Graphics g){ + if (g != null){ + FontMetrics fm = g.getFontMetrics(getColoring().getFont()); + if (fm != null){ + int ret = fm.getAscent() - fm.getDescent(); + return ret - ret%2; + } + } + return -1; + } + + private boolean hasMousePoint() { + return mousePoint >= 0; + } + + private boolean isActivated(int y1, int y2) { + return hasMousePoint() && + (mousePoint >= y1 && mousePoint < y2); + } + + private void drawFoldLine(Graphics2D g2d, boolean active, int x1, int y1, int x2, int y2) { + Stroke origStroke = g2d.getStroke(); + g2d.setStroke(getStroke(origStroke, active)); + g2d.drawLine(x1, y1, x2, y2); + g2d.setStroke(origStroke); + } + + protected @Override void paintComponent(Graphics g) { + if (!enabled) { + return; + } + + Rectangle clip = getVisibleRect();//g.getClipBounds(); + visibleMarks.clear(); + + Coloring coloring = getColoring(); + g.setColor(coloring.getBackColor()); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + g.setColor(coloring.getForeColor()); + + AbstractDocument adoc = (AbstractDocument)component.getDocument(); + adoc.readLock(); + try { + List ps = getPaintInfo(clip); + Font defFont = coloring.getFont(); + int markSize = getMarkSize(g); + int halfMarkSize = markSize / 2; + int markX = (defFont.getSize() - markSize) / 2; // x position of mark rectangle + int plusGap = (int)Math.round(markSize / 3.8); // distance between mark rectangle vertical side and start/end of minus sign + int lineX = markX + halfMarkSize; // x position of the centre of mark + + LOG.fine("CFSBar: PAINT START ------\n"); + int descent = g.getFontMetrics(defFont).getDescent(); + PaintInfo previousInfo = null; + Graphics2D g2d = (Graphics2D)g; + LOG.log(Level.FINEST, "MousePoint: {0}", mousePoint); + + for(PaintInfo paintInfo : ps) { + boolean isFolded = paintInfo.isCollapsed(); + int y = paintInfo.getPaintY(); + int height = paintInfo.getPaintHeight(); + int markY = y + descent; // y position of mark rectangle + int paintOperation = paintInfo.getPaintOperation(); + + if (previousInfo == null) { + if (paintInfo.hasLineIn()) { + if (LOG.isLoggable(Level.FINE)) { + LOG.fine("prevInfo=NULL; y=" + y + ", PI:" + paintInfo + "\n"); // NOI18N + } + drawFoldLine(g2d, paintInfo.lineInActive, lineX, clip.y, lineX, y); + } + } else { + if (previousInfo.hasLineOut() || paintInfo.hasLineIn()) { + // Draw middle vertical line + int prevY = previousInfo.getPaintY(); + if (LOG.isLoggable(Level.FINE)) { + LOG.log(Level.FINE, "prevInfo={0}; y=" + y + ", PI:" + paintInfo + "\n", previousInfo); // NOI18N + } + drawFoldLine(g2d, previousInfo.lineOutActive || paintInfo.lineInActive, lineX, prevY + previousInfo.getPaintHeight(), lineX, y); + } + } + + if (paintInfo.hasSign()) { + g.drawRect(markX, markY, markSize, markSize); + g.drawLine(plusGap + markX, markY + halfMarkSize, markSize + markX - plusGap, markY + halfMarkSize); + String opStr = (paintOperation == PAINT_MARK) ? "PAINT_MARK" : "SINGLE_PAINT_MARK"; // NOI18N + if (isFolded) { + if (LOG.isLoggable(Level.FINE)) { + LOG.fine(opStr + ": folded; y=" + y + ", PI:" + paintInfo + "\n"); // NOI18N + } + g.drawLine(lineX, markY + plusGap, lineX, markY + markSize - plusGap); + } + if (paintOperation != SINGLE_PAINT_MARK) { + if (LOG.isLoggable(Level.FINE)) { + LOG.fine(opStr + ": non-single; y=" + y + ", PI:" + paintInfo + "\n"); // NOI18N + } + } + if (paintInfo.hasLineIn()) { //[PENDING] + drawFoldLine(g2d, paintInfo.lineInActive, lineX, y, lineX, markY); + } + if (paintInfo.hasLineOut()) { + // This is an error in case there's a next paint info at the same y which is an end mark + // for this mark (it must be cleared explicitly). + drawFoldLine(g2d, paintInfo.lineOutActive, lineX, markY + markSize, lineX, y + height); + } + visibleMarks.add(new Mark(markX, markY, markSize, isFolded)); + + } else if (paintOperation == PAINT_LINE) { + if (LOG.isLoggable(Level.FINE)) { + LOG.fine("PAINT_LINE: y=" + y + ", PI:" + paintInfo + "\n"); // NOI18N + } + // FIXME !! + drawFoldLine(g2d, paintInfo.signActive, lineX, y, lineX, y + height ); + } else if (paintOperation == PAINT_END_MARK) { + if (LOG.isLoggable(Level.FINE)) { + LOG.fine("PAINT_END_MARK: y=" + y + ", PI:" + paintInfo + "\n"); // NOI18N + } + if (previousInfo == null || y != previousInfo.getPaintY()) { + drawFoldLine(g2d, paintInfo.lineInActive, lineX, y, lineX, y + height / 2); + drawFoldLine(g2d, paintInfo.signActive, lineX, y + height / 2, lineX + halfMarkSize, y + height / 2); + if (paintInfo.getInnerLevel() > 0) {//[PENDING] + if (LOG.isLoggable(Level.FINE)) { + LOG.fine(" PAINT middle-line\n"); // NOI18N + } + drawFoldLine(g2d, paintInfo.lineOutActive, lineX, y + height / 2, lineX, y + height); + } + } + } + + previousInfo = paintInfo; + } + + if (previousInfo != null && + (previousInfo.getInnerLevel() > 0 || + (previousInfo.getPaintOperation() == PAINT_MARK && !previousInfo.isCollapsed())) + ) { + drawFoldLine(g2d, previousInfo.lineOutActive, + lineX, previousInfo.getPaintY() + previousInfo.getPaintHeight(), lineX, clip.y + clip.height); + } + + } catch (BadLocationException ble) { + LOG.log(Level.WARNING, null, ble); + } finally { + LOG.fine("CFSBar: PAINT END ------\n\n"); + adoc.readUnlock(); + } + } + + private static Object [] getFoldList(Fold parentFold, int start, int end) { + List ret = new ArrayList(); + + int index = FoldUtilities.findFoldEndIndex(parentFold, start); + int foldCount = parentFold.getFoldCount(); + int idxOfFirstFoldStartingInside = -1; + while (index < foldCount) { + Fold f = parentFold.getFold(index); + if (f.getStartOffset() <= end) { + ret.add(f); + } else { + break; // no more relevant folds + } + if (idxOfFirstFoldStartingInside == -1 && f.getStartOffset() >= start) { + idxOfFirstFoldStartingInside = ret.size() - 1; + } + index++; + } + + return new Object [] { ret, idxOfFirstFoldStartingInside != -1 ? idxOfFirstFoldStartingInside : ret.size() }; + } + + /** + * This class should be never used by other code; will be made private + */ + public class PaintInfo { + + int paintOperation; + /** + * level of the 1st marker on the line + */ + int innerLevel; + + /** + * Y-coordinate of the cell + */ + int paintY; + + /** + * Height of the paint cell + */ + int paintHeight; + + /** + * State of the marker (+/-) + */ + boolean isCollapsed; + + /** + * all markers on the line are collapsed + */ + boolean allCollapsed; + int startOffset; + int endOffset; + /** + * nesting level of the last marker on the line + */ + int outgoingLevel; + + /** + * Force incoming line (from above) to be present + */ + boolean lineIn; + + /** + * Force outgoing line (down from marker) to be present + */ + boolean lineOut; + + /** + * The 'incoming' (upper) line should be painted as active + */ + boolean lineInActive; + + /** + * The 'outgoing' (down) line should be painted as active + */ + boolean lineOutActive; + + /** + * The sign/marker itself should be painted as active + */ + boolean signActive; + + public PaintInfo(int paintOperation, int innerLevel, int paintY, int paintHeight, boolean isCollapsed, int startOffset, int endOffset){ + this.paintOperation = paintOperation; + this.innerLevel = this.outgoingLevel = innerLevel; + this.paintY = paintY; + this.paintHeight = paintHeight; + this.isCollapsed = this.allCollapsed = isCollapsed; + this.startOffset = startOffset; + this.endOffset = endOffset; + + switch (paintOperation) { + case PAINT_MARK: + lineIn = false; + lineOut = true; + outgoingLevel++; + break; + case SINGLE_PAINT_MARK: + lineIn = false; + lineOut = false; + break; + case PAINT_END_MARK: + lineIn = true; + lineOut = false; + isCollapsed = true; + allCollapsed = true; + break; + case PAINT_LINE: + lineIn = lineOut = true; + break; + } + } + + /** + * Sets active flags on inidivual parts of the mark + * @param mark + * @param lineIn + * @param lineOut S + */ + void markActive(boolean mark, boolean lineIn, boolean lineOut) { + this.signActive |= mark; + this.lineInActive |= lineIn; + this.lineOutActive |= lineOut; + } + + boolean hasLineIn() { + return lineIn || innerLevel > 0; + } + + boolean hasLineOut() { + return lineOut || outgoingLevel > 0 || (paintOperation != SINGLE_PAINT_MARK && !isAllCollapsed()); + } + + public PaintInfo(int paintOperation, int innerLevel, int paintY, int paintHeight, int startOffset, int endOffset){ + this(paintOperation, innerLevel, paintY, paintHeight, false, startOffset, endOffset); + } + + public int getPaintOperation(){ + return paintOperation; + } + + public int getInnerLevel(){ + return innerLevel; + } + + public int getPaintY(){ + return paintY; + } + + public int getPaintHeight(){ + return paintHeight; + } + + public boolean isCollapsed(){ + return isCollapsed; + } + + boolean isAllCollapsed() { + return allCollapsed; + } + + public void setPaintOperation(int paintOperation){ + this.paintOperation = paintOperation; + } + + public void setInnerLevel(int innerLevel){ + this.innerLevel = innerLevel; + } + + public @Override String toString(){ + StringBuffer sb = new StringBuffer(""); + if (paintOperation == PAINT_MARK){ + sb.append("PAINT_MARK"); // NOI18N + }else if (paintOperation == PAINT_LINE){ + sb.append("PAINT_LINE"); // NOI18N + }else if (paintOperation == PAINT_END_MARK) { + sb.append("PAINT_END_MARK"); // NOI18N + }else if (paintOperation == SINGLE_PAINT_MARK) { + sb.append("SINGLE_PAINT_MARK"); + } + sb.append(",L:").append(innerLevel).append("/").append(outgoingLevel); // NOI18N + sb.append(',').append(isCollapsed ? "C" : "E"); // NOI18N + sb.append(", start=").append(startOffset).append(", end=").append(endOffset); + sb.append(", lineIn=").append(lineIn).append(", lineOut=").append(lineOut); + return sb.toString(); + } + + boolean hasSign() { + return paintOperation == PAINT_MARK || paintOperation == SINGLE_PAINT_MARK; + } + + + void mergeWith(PaintInfo prevInfo) { + if (prevInfo == null) { + return; + } + + int operation = this.paintOperation; + boolean lineIn = prevInfo.lineIn; + boolean lineOut = prevInfo.lineOut; + + LOG.log(Level.FINE, "Merging {0} with {1}: ", new Object[] { this, prevInfo }); + if (prevInfo.getPaintOperation() == PAINT_END_MARK) { + // merge with start|single -> start mark + line-in + lineIn = true; + } else { + operation = PAINT_MARK; + } + + int level1 = Math.min(prevInfo.innerLevel, innerLevel); + int level2 = prevInfo.outgoingLevel; + + if (getPaintOperation() == PAINT_END_MARK + && innerLevel == prevInfo.outgoingLevel) { + // if merging end marker at the last level, update to the new outgoing level + level2 = outgoingLevel; + } else if (!isCollapsed) { + level2 = Math.max(prevInfo.outgoingLevel, outgoingLevel); + } + + if (prevInfo.getInnerLevel() < getInnerLevel()) { + int paintFrom = Math.min(prevInfo.paintY, paintY); + int paintTo = Math.max(prevInfo.paintY + prevInfo.paintHeight, paintY + paintHeight); + // at least one collapsed -> paint plus sign + boolean collapsed = prevInfo.isCollapsed() || isCollapsed(); + int offsetFrom = Math.min(prevInfo.startOffset, startOffset); + int offsetTo = Math.max(prevInfo.endOffset, endOffset); + + this.paintY = paintFrom; + this.paintHeight = paintTo - paintFrom; + this.isCollapsed = collapsed; + this.startOffset = offsetFrom; + this.endOffset = offsetTo; + } + this.paintOperation = operation; + this.allCollapsed = prevInfo.allCollapsed && allCollapsed; + this.innerLevel = level1; + this.outgoingLevel = level2; + this.lineIn |= lineIn; + this.lineOut |= lineOut; + + this.signActive |= prevInfo.signActive; + this.lineInActive |= prevInfo.lineInActive; + this.lineOutActive |= prevInfo.lineOutActive; + + LOG.log(Level.FINE, "Merged result: {0}", this); + } + } + + /** Keeps info of visible folding mark */ + public class Mark{ + public int x; + public int y; + public int size; + public boolean isFolded; + + public Mark(int x, int y, int size, boolean isFolded){ + this.x = x; + this.y = y; + this.size = size; + this.isFolded = isFolded; + } + } + + private final class Listener extends MouseAdapter implements FoldHierarchyListener, DocumentListener, Runnable { + + public Listener(){ + } + + // -------------------------------------------------------------------- + // FoldHierarchyListener implementation + // -------------------------------------------------------------------- + + public void foldHierarchyChanged(FoldHierarchyEvent evt) { + refresh(); + } + + // -------------------------------------------------------------------- + // DocumentListener implementation + // -------------------------------------------------------------------- + + public void insertUpdate(DocumentEvent evt) { + if (!(evt instanceof BaseDocumentEvent)) return; + + BaseDocumentEvent bevt = (BaseDocumentEvent)evt; + if (bevt.getLFCount() > 0) { // one or more lines inserted + refresh(); + } + } + + public void removeUpdate(DocumentEvent evt) { + if (!(evt instanceof BaseDocumentEvent)) return; + + BaseDocumentEvent bevt = (BaseDocumentEvent)evt; + if (bevt.getLFCount() > 0) { // one or more lines removed + refresh(); + } + } + + public void changedUpdate(DocumentEvent evt) { + } + + // -------------------------------------------------------------------- + // MouseListener implementation + // -------------------------------------------------------------------- + + @Override + public void mousePressed (MouseEvent e) { + Mark mark = getClickedMark(e); + if (mark!=null){ + e.consume(); + performAction(mark, (e.getModifiersEx() & MouseEvent.CTRL_DOWN_MASK) > 0); + } + } + + @Override + public void mouseClicked(MouseEvent e) { + // #102288 - missing event consuming caused quick doubleclicks to break + // fold expanding/collapsing and move caret to the particular line + if (e.getClickCount() > 1) { + LOG.log(Level.FINEST, "Mouse {0}click at {1}", new Object[] { e.getClickCount(), e.getY()}); + Mark mark = getClickedMark(e); + try { + performActionAt(mark, e.getY()); + } catch (BadLocationException ex) { + LOG.log(Level.WARNING, "Error during fold expansion using sideline", ex); + } + } else { + e.consume(); + } + } + + private void refreshIfMouseOutside(Point pt) { + mousePoint = (int)pt.getY(); + if (LOG.isLoggable(Level.FINEST)) { + if (mouseBoundary == null) { + LOG.log(Level.FINEST, "Mouse boundary not set, refreshing: {0}", mousePoint); + } else { + LOG.log(Level.FINEST, "Mouse {0} inside known mouse boundary: {1}-{2}", + new Object[] { mousePoint, mouseBoundary.y, mouseBoundary.getMaxY() }); + } + } + if (mouseBoundary == null || mousePoint < mouseBoundary.y || mousePoint > mouseBoundary.getMaxY()) { + refresh(); + } + } + + @Override + public void mouseMoved(MouseEvent e) { + refreshIfMouseOutside(e.getPoint()); + } + + public void mouseEntered(MouseEvent e) { + refreshIfMouseOutside(e.getPoint()); + } + + public void mouseExited(MouseEvent e) { + mousePoint = NO_MOUSE_POINT; + refresh(); + } + + + // -------------------------------------------------------------------- + // private implementation + // -------------------------------------------------------------------- + + private Mark getClickedMark(MouseEvent e){ + if (e == null || !SwingUtilities.isLeftMouseButton(e)) { + return null; + } + + int x = e.getX(); + int y = e.getY(); + for (Mark mark : visibleMarks) { + if (x >= mark.x && x <= (mark.x + mark.size) && y >= mark.y && y <= (mark.y + mark.size)) { + return mark; + } + } + return null; + } + + private void refresh() { + SwingUtilities.invokeLater(this); + } + + @Override + public void run() { + if (getPreferredSize().width == 0 && enabled) { + updatePreferredSize(); + } + repaint(); + } + } // End of Listener class + + @Override + public AccessibleContext getAccessibleContext() { + if (accessibleContext == null) { + accessibleContext = new AccessibleJComponent() { + public @Override AccessibleRole getAccessibleRole() { + return AccessibleRole.PANEL; + } + }; + accessibleContext.setAccessibleName(NbBundle.getMessage(CodeFoldingSideBar.class, "ACSN_CodeFoldingSideBar")); //NOI18N + accessibleContext.setAccessibleDescription(NbBundle.getMessage(CodeFoldingSideBar.class, "ACSD_CodeFoldingSideBar")); //NOI18N + } + return accessibleContext; + } + + private Coloring getColoring() { + if (attribs == null) { + if (fcsLookupResult == null) { + fcsLookupResult = MimeLookup.getLookup(org.netbeans.lib.editor.util.swing.DocumentUtilities.getMimeType(component)) + .lookupResult(FontColorSettings.class); + fcsLookupResult.addLookupListener(WeakListeners.create(LookupListener.class, fcsTracker, fcsLookupResult)); + } + + FontColorSettings fcs = fcsLookupResult.allInstances().iterator().next(); + AttributeSet attr = fcs.getFontColors(FontColorNames.CODE_FOLDING_BAR_COLORING); + if (attr == null) { + attr = fcs.getFontColors(FontColorNames.DEFAULT_COLORING); + } else { + attr = AttributesUtilities.createComposite(attr, fcs.getFontColors(FontColorNames.DEFAULT_COLORING)); + } + attribs = attr; + } + return Coloring.fromAttributeSet(attribs); + } + + /** + * Factory for the sidebars. Use {@link FoldUtilities#getFoldingSidebarFactory()} to + * obtain an instance. + */ + public static class Factory implements SideBarFactory { + @Override + public JComponent createSideBar(JTextComponent target) { + return new CodeFoldingSideBar(target); + } + } +} diff --git a/editor.fold/src/org/netbeans/modules/editor/fold/ui/CustomizerWithDefaults.java b/editor.fold/src/org/netbeans/modules/editor/fold/ui/CustomizerWithDefaults.java new file mode 100644 --- /dev/null +++ b/editor.fold/src/org/netbeans/modules/editor/fold/ui/CustomizerWithDefaults.java @@ -0,0 +1,62 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2013 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + * + * Contributor(s): + * + * Portions Copyrighted 2013 Sun Microsystems, Inc. + */ +package org.netbeans.modules.editor.fold.ui; + +import java.util.prefs.Preferences; + +/** + * Mixin interface, that allows to configure a Customizer to be configured with parent preferences. + * The parent preferences may be either defaults stored or predefined, or preferences for the parent Mime type. + * The customizer must not write to the default preferences + * + * @author sdedic + */ +public interface CustomizerWithDefaults { + /** + * Provides default preferences to the customizer. All language preferences, + * or text/xml preferences are passed this way to the language customizer, + * to serve as a basis. The default preferences + * + * @param pref default preferences + */ + public void setDefaultPreferences(Preferences pref); +} diff --git a/editor.fold/src/org/netbeans/modules/editor/fold/ui/DefaultFoldingOptions.form b/editor.fold/src/org/netbeans/modules/editor/fold/ui/DefaultFoldingOptions.form new file mode 100644 --- /dev/null +++ b/editor.fold/src/org/netbeans/modules/editor/fold/ui/DefaultFoldingOptions.form @@ -0,0 +1,59 @@ + + +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/editor.fold/src/org/netbeans/modules/editor/fold/ui/DefaultFoldingOptions.java b/editor.fold/src/org/netbeans/modules/editor/fold/ui/DefaultFoldingOptions.java new file mode 100644 --- /dev/null +++ b/editor.fold/src/org/netbeans/modules/editor/fold/ui/DefaultFoldingOptions.java @@ -0,0 +1,463 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2013 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + * + * Contributor(s): + * + * Portions Copyrighted 2013 Sun Microsystems, Inc. + */ +package org.netbeans.modules.editor.fold.ui; + +import java.awt.GridLayout; +import java.awt.event.ItemEvent; +import java.awt.event.ItemListener; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.prefs.PreferenceChangeEvent; +import java.util.prefs.PreferenceChangeListener; +import java.util.prefs.Preferences; +import javax.swing.JCheckBox; +import javax.swing.JComponent; +import javax.swing.SwingUtilities; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import org.netbeans.api.editor.fold.FoldType; +import org.netbeans.api.editor.fold.FoldUtilities; +import org.netbeans.api.editor.mimelookup.MimePath; +import org.netbeans.api.editor.settings.SimpleValueNames; +import org.netbeans.modules.editor.fold.FoldUtilitiesImpl; +import org.netbeans.modules.editor.settings.storage.api.EditorSettings; +import org.netbeans.modules.editor.settings.storage.api.OverridePreferences; +import org.openide.util.WeakListeners; + +/** + * Default implementation of folding options. The dialog will only display + * checboxes, for each of the registered FoldTypes. FoldTypes are displayed + * in same order, as they are enumerated by FoldUtilities.getFoldTypes(). + *

+ * The dialog will read and set preference values {@code code-folding-collapse-X}, + * where X is obtained from {@link FoldType#code()}. + * + * @author sdedic + */ +public final class DefaultFoldingOptions extends javax.swing.JPanel +implements PreferenceChangeListener, ChangeListener, CustomizerWithDefaults, ItemListener { + + private static final Logger LOG = Logger.getLogger(DefaultFoldingOptions.class.getName()); + + public static final String COLLAPSE_PREFIX = FoldUtilitiesImpl.PREF_COLLAPSE_PREFIX; + + public static final String PREF_OVERRIDE_DEFAULTS = FoldUtilitiesImpl.PREF_OVERRIDE_DEFAULTS; + + private static final Set LEGACY_FOLD_TYPES = new HashSet(); + + static { + LEGACY_FOLD_TYPES.add(FoldType.CODE_BLOCK); + LEGACY_FOLD_TYPES.add(FoldType.INITIAL_COMMENT); + LEGACY_FOLD_TYPES.add(FoldType.DOCUMENTATION); + LEGACY_FOLD_TYPES.add(FoldType.TAG); + LEGACY_FOLD_TYPES.add(FoldType.MEMBER); + LEGACY_FOLD_TYPES.add(FoldType.NESTED); + LEGACY_FOLD_TYPES.add(FoldType.IMPORT); + } + + /** + * Mimetype for which these options work + */ + private String mimeType; + + /** + * Transient preferences, contains current values + defaults. + */ + private Preferences preferences; + + /** + * Preferences for the super mimetype (or "") + */ + private Preferences defaultPrefs; + + /** + * Fold types registered for this MIME type. For "" type, the fold types + * are filtered for those, which are actually used by any of the registered MIME types + */ + private Collection types; + + /** + * Checkbox controls for collapse-* options + */ + private Collection controls = new ArrayList(); + + private PreferenceChangeListener weakL; + + private Collection parentFoldTypes; + + /** + * Creates new form DefaultFoldingOptions + */ + public DefaultFoldingOptions(String mime, Preferences preferences) { + initComponents(); + + VerticalFlowLayout vfl = new VerticalFlowLayout(); + localSwitchboard.setLayout(vfl); + + vfl = new VerticalFlowLayout(); + localSwitchboard.setLayout(vfl); + + this.mimeType = mime; + this.preferences = preferences; + + String parentMime = MimePath.parse(mime).getInheritedType(); + if (parentMime != null) { + parentFoldTypes = new HashSet(13); + for (FoldType ft : FoldUtilities.getFoldTypes(parentMime).values()) { + parentFoldTypes.add(ft.code()); + } + } else { + parentFoldTypes = Collections.emptyList(); + } + } + + @Override + public void setDefaultPreferences(Preferences pref) { + if (this.defaultPrefs != null) { + defaultPrefs.removePreferenceChangeListener(weakL); + } + // anomaly: if the local Preference contains 'removedKey' for a preference + // which has been turned to default, it blocks all change propagations. + this.defaultPrefs = pref; + if (pref != null) { + weakL = WeakListeners.create(PreferenceChangeListener.class, this, pref); + pref.addPreferenceChangeListener(weakL); + } + } + + private static String k(FoldType ft) { + return COLLAPSE_PREFIX + ft.code(); + } + + private JCheckBox createCheckBox(FoldType ft) { + return new JCheckBox(); + } + + /** + * Filters types to contain only mimetypes, which are actually used in + * one or more mimetypes. Used only for "" mimetype (defaults). + */ + private void filterUsedMimeTypes() { + Set mimeTypes = EditorSettings.getDefault().getAllMimeTypes(); + Set codes = new HashSet(); + for (String mt : mimeTypes) { + Collection fts = FoldUtilities.getFoldTypes(mt).values(); + for (FoldType ft : fts) { + codes.add(ft.code()); + if (ft.parent() != null) { + codes.add(ft.parent().code()); + } + } + } + for (Iterator it = types.iterator(); it.hasNext();) { + FoldType ft = it.next(); + if (LEGACY_FOLD_TYPES.contains(ft)) { + continue; + } + if (!codes.contains(ft.code())) { + it.remove(); + } + } + } + + private void load() { + types = new ArrayList(FoldUtilities.getFoldTypes(mimeType).values()); + if ("".equals(mimeType)) { // NOI18N + filterUsedMimeTypes(); + } + + boolean override = isCollapseRedefined(); + boolean useDefaultsState = + !(preferences.getBoolean(PREF_OVERRIDE_DEFAULTS, false) || + isDefinedLocally(PREF_OVERRIDE_DEFAULTS)); + if (override != useDefaultsState) { + updateOverrideChanged(); + } + + for (FoldType ft : types) { + String name = ft.getLabel(); + + JCheckBox cb = createCheckBox(ft); + cb.setText(name); + cb.putClientProperty("id", ft.code()); // NOI18N + cb.putClientProperty("type", ft); // NOI18N + localSwitchboard.add(cb); + controls.add(cb); + cb.addItemListener(this); + } + + // watch out for preferences + this.preferences.addPreferenceChangeListener(this); + updateEnabledState(); + } + + /** + * Checks whether some of the collapse- options is redefined for this MIME type. + * If so, the checkbox will be selected even though the override option is not + * present. + * + * @return + */ + private boolean isCollapseRedefined() { + for (FoldType ft : types) { + String pref = k(ft); + if (((OverridePreferences)preferences).isOverriden(pref)) { + if (defaultPrefs == null || + (parentFoldTypes.contains(ft.code()) || (ft.parent() != null && parentFoldTypes.contains(ft.parent().code())))) { + return true; + } + } + } + return false; + } + + private boolean loaded; + + @Override + public void addNotify() { + super.addNotify(); + if (!loaded) { + load(); + updateEnabledState(); + updateValueState(); + loaded = true; + } + } + + @Override + public void preferenceChange(final PreferenceChangeEvent evt) { + SwingUtilities.invokeLater(new Runnable() { + public void run() { + updateCheckers(evt); + } + }); + } + + private void updateValueState() { + ignoreStateChange = true; + for (JCheckBox cb : controls) { + FoldType ft = (FoldType)cb.getClientProperty("type"); // NOI18N + String k = COLLAPSE_PREFIX + ft.code(); + boolean val = isCollapseEnabled(ft); + cb.setSelected(val); + } + ignoreStateChange = false; + } + + private void updateEnabledState() { + boolean foldEnable = preferences.getBoolean(SimpleValueNames.CODE_FOLDING_ENABLE, true); + boolean useDefaults = preferences.getBoolean(FoldUtilitiesImpl.PREF_OVERRIDE_DEFAULTS, true); + + for (JComponent c : controls) { + FoldType ft = (FoldType)c.getClientProperty("type"); // NOI18N + boolean enable = foldEnable; + if (defaultPrefs != null && useDefaults) { + if (!parentFoldTypes.contains(ft.code())) { + if (ft.parent() == null || !parentFoldTypes.contains(ft.parent().code())) { + continue; + } + } + enable &= !isDefinedDefault(ft); + } + c.setEnabled(enable); + } + } + + private void updateCheckers(PreferenceChangeEvent evt) { + String pk = evt.getKey(); + if (pk.equals(SimpleValueNames.CODE_FOLDING_ENABLE)) { + updateEnabledState(); + return; + } + if (pk.equals(PREF_OVERRIDE_DEFAULTS)) { + updateOverrideChanged(); + } else if (!pk.startsWith(COLLAPSE_PREFIX)) { + return; + } + String c = pk.substring(COLLAPSE_PREFIX.length()); + for (JCheckBox cb : controls) { + FoldType ft = (FoldType)cb.getClientProperty("type"); // NOI18N + FoldType ftp = ft.parent(); + if (ft.code().equals(c) || (ftp != null && ftp.code().equals(c))) { + updateChecker(pk, cb, ft); + return; + } + } + } + + private boolean isCollapseEnabled(FoldType ft) { + if (defaultPrefs == null) { + return preferences.getBoolean(k(ft), + ft.parent() == null ? false + : preferences.getBoolean(k(ft.parent()), false)); + } else { + String k = k(ft); + return preferences.getBoolean(k, + defaultPrefs.getBoolean(k, + ft.parent() == null ? false + : preferences.getBoolean(k(ft.parent()), false))); + } + } + + private void updateOverrideChanged() { + boolean en = preferences.getBoolean(FoldUtilitiesImpl.PREF_OVERRIDE_DEFAULTS, true); + if (defaultPrefs == null) { + return; + } + if (en) { + // persist all foldtype settings: + for (FoldType ft : types) { + preferences.putBoolean(k(ft), + defaultPrefs.getBoolean(k(ft), + ft.parent() == null ? false : + defaultPrefs.getBoolean(k(ft.parent()), false)) + ); + } + } else { + for (FoldType ft : types) { + if (isDefinedDefault(ft)) { + preferences.remove(k(ft)); + } + } + } + updateEnabledState(); + updateValueState(); + } + + private boolean isDefinedDefault(FoldType ft) { + return parentFoldTypes.contains(ft.code()) || + (ft.parent() != null && parentFoldTypes.contains(ft.parent().code())); + } + + private boolean isDefinedLocally(String prefKey) { + return !(preferences instanceof OverridePreferences) || + ((OverridePreferences)preferences).isOverriden(prefKey); + } + + private void updateChecker(String prefKey, JCheckBox cb, FoldType ft) { + if (lastChangedCB == cb) { + // ignore + lastChangedCB = null; + return; + } + boolean val = isCollapseEnabled(ft); + ignoreStateChange = true; + LOG.log(Level.INFO, "Updating checker: " + prefKey + ", setSelected " + val); + cb.setSelected(val); + ignoreStateChange = false; + } + + private boolean ignoreStateChange; + + private JCheckBox lastChangedCB; + + @Override + public void itemStateChanged(final ItemEvent e) { + if (ignoreStateChange) { + return; + } + SwingUtilities.invokeLater(new Runnable() { + public void run() { + updatePref(e); + } + }); + } + + private void updatePref(ItemEvent e) { + JCheckBox cb = (JCheckBox)e.getSource(); + FoldType ft = (FoldType)cb.getClientProperty("type"); // NOI18N + + String prefKey = COLLAPSE_PREFIX + ft.code(); + lastChangedCB = cb; + LOG.log(Level.INFO, "Updating preference: " + prefKey + ", value = " + cb.isSelected()); + preferences.putBoolean(prefKey, cb.isSelected()); + } + + @Override + public void stateChanged(ChangeEvent e) { + if (ignoreStateChange) { + return; + } + } + + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + @SuppressWarnings("unchecked") + // //GEN-BEGIN:initComponents + private void initComponents() { + + collapseContainer = new javax.swing.JPanel(); + localSwitchboard = new javax.swing.JPanel(); + + collapseContainer.setBorder(javax.swing.BorderFactory.createTitledBorder(org.openide.util.NbBundle.getMessage(DefaultFoldingOptions.class, "DefaultFoldingOptions.collapseContainer.border.title"))); // NOI18N + collapseContainer.setLayout(new java.awt.BorderLayout()); + + localSwitchboard.setLayout(null); + collapseContainer.add(localSwitchboard, java.awt.BorderLayout.CENTER); + + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); + this.setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(collapseContainer, javax.swing.GroupLayout.DEFAULT_SIZE, 180, Short.MAX_VALUE) + ); + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(collapseContainer, javax.swing.GroupLayout.DEFAULT_SIZE, 151, Short.MAX_VALUE) + ); + }// //GEN-END:initComponents + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JPanel collapseContainer; + private javax.swing.JPanel localSwitchboard; + // End of variables declaration//GEN-END:variables +} diff --git a/editor.fold/src/org/netbeans/modules/editor/fold/ui/FoldOptionsController.java b/editor.fold/src/org/netbeans/modules/editor/fold/ui/FoldOptionsController.java new file mode 100644 --- /dev/null +++ b/editor.fold/src/org/netbeans/modules/editor/fold/ui/FoldOptionsController.java @@ -0,0 +1,237 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2013 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + * + * Contributor(s): + * + * Portions Copyrighted 2013 Sun Microsystems, Inc. + */ +package org.netbeans.modules.editor.fold.ui; + +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.util.HashMap; +import java.util.Map; +import java.util.prefs.BackingStoreException; +import java.util.prefs.PreferenceChangeEvent; +import java.util.prefs.PreferenceChangeListener; +import java.util.prefs.Preferences; +import javax.swing.JComponent; +import org.netbeans.api.editor.mimelookup.MimeLookup; +import org.netbeans.api.editor.mimelookup.MimePath; +import org.netbeans.api.editor.settings.SimpleValueNames; +import org.netbeans.api.options.OptionsDisplayer; +import org.netbeans.modules.editor.settings.storage.api.EditorSettings; +import org.netbeans.modules.editor.settings.storage.api.MemoryPreferences; +import org.netbeans.spi.options.OptionsPanelController; +import org.openide.util.Exceptions; +import org.openide.util.HelpCtx; +import org.openide.util.Lookup; +import org.openide.util.WeakListeners; + +/** + * Controller for the Folding options tab. + * The controller manages the MemoryPreferences storage, which is then passed to individual folding + * customizer panels. It also tracks 'dirty' status of all the language panels: if one of them becomes dirty, + * the whole page starts to report dirty = true. + *

+ * This class manages only the language switch + overall 'enable folding' setting. All settings + * for a given language are handled by the language-specific panel. + * + * @author sdedic + */ +@OptionsPanelController.SubRegistration( + displayName="org.netbeans.modules.editor.fold.ui.Bundle#CTL_OptionsDisplayName", + keywords="org.netbeans.modules.editor.fold.ui.Bundle#KW_Options", + keywordsCategory="Editor/Folding", + id="Folding", // XXX used anywhere? + location=OptionsDisplayer.EDITOR, + position=110 +// toolTip="org.netbeans.modules.options.editor.Bundle#CTL_General_ToolTip" +) +public class FoldOptionsController extends OptionsPanelController implements PreferenceChangeListener { + /** + * The main panel. + */ + private FoldOptionsPanel panel; + + /** + * True, if some of the created panels became dirty + */ + private boolean changed; + + /** + * for firing PROP_DIRTY + */ + private PropertyChangeSupport propSupport = new PropertyChangeSupport(this); + + /** + * Preferences created for individual MIME types, as they are displaye by the user + */ + private Map preferences = new HashMap(); + + @Override + public void update() { + clearContents(); + if (panel != null) { + panel.update(); + } + } + + @Override + public void applyChanges() { + for (MemoryPreferences p : preferences.values()) { + try { + p.getPreferences().flush(); + } catch (BackingStoreException ex) { + Exceptions.printStackTrace(ex); + } + } + changed = false; + propSupport.firePropertyChange(PROP_CHANGED, true, false); + } + + @Override + public void preferenceChange(PreferenceChangeEvent evt) { + boolean ch = detectIsChanged(); + if (ch != changed) { + changed = ch; + propSupport.firePropertyChange(PROP_CHANGED, !ch, ch); + } + } + + private PreferenceChangeListener weakChangeL = WeakListeners.create(PreferenceChangeListener.class, this, null); + + void globalEnableFolding(boolean enable) { + prefs("").putBoolean(SimpleValueNames.CODE_FOLDING_ENABLE, enable); + for (String mime : EditorSettings.getDefault().getAllMimeTypes()) { + prefs(mime).remove(SimpleValueNames.CODE_FOLDING_ENABLE); + } + } + + /* called from the FoldOptionsPanel */ + Preferences prefs(String mime) { + MemoryPreferences cached = preferences.get(mime); + if (cached != null) { + return cached.getPreferences(); + } + MimePath path = MimePath.parse(mime); + Preferences result = MimeLookup.getLookup(mime).lookup(Preferences.class); + + if (!mime.equals("")) { + String parentMime = path.getInheritedType(); + /* + result = new InheritedPreferences( + prefs(parentMime), result); + */ + cached = MemoryPreferences.getWithInherited(this, + prefs(parentMime), + result); + } else { + cached = MemoryPreferences.get(this, result); + } + cached.getPreferences().addPreferenceChangeListener(weakChangeL); + preferences.put(mime, cached); + return cached.getPreferences(); + } + + private void clearContents() { + // clear the old preference values and recreate the panel + for (MemoryPreferences m : preferences.values()) { + m.getPreferences().removePreferenceChangeListener(weakChangeL); + m.destroy(); + } + preferences.clear(); + if (panel != null) { + panel.clear(); + } + changed = false; + } + + @Override + public void cancel() { + clearContents(); + } + + @Override + public boolean isValid() { + return true; + + } + + @Override + public boolean isChanged() { + return changed; + + } + + private boolean detectIsChanged() { + for (MemoryPreferences cached : preferences.values()) { + if (!cached.isDirty(cached.getPreferences())) { + return true; + } + } + return false; + } + + @Override + public JComponent getComponent(Lookup masterLookup) { + return getPanel(); + } + + @Override + public HelpCtx getHelpCtx() { + return new HelpCtx ("netbeans.optionsDialog.editor.folding"); + } + + @Override + public void addPropertyChangeListener(PropertyChangeListener l) { + propSupport.addPropertyChangeListener(l); + } + + @Override + public void removePropertyChangeListener(PropertyChangeListener l) { + propSupport.removePropertyChangeListener(l); + } + + private FoldOptionsPanel getPanel() { + if (panel == null) { + panel = new FoldOptionsPanel(this); + } + return panel; + } + +} diff --git a/editor.fold/src/org/netbeans/modules/editor/fold/ui/FoldOptionsPanel.form b/editor.fold/src/org/netbeans/modules/editor/fold/ui/FoldOptionsPanel.form new file mode 100644 --- /dev/null +++ b/editor.fold/src/org/netbeans/modules/editor/fold/ui/FoldOptionsPanel.form @@ -0,0 +1,155 @@ + + +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/editor.fold/src/org/netbeans/modules/editor/fold/ui/FoldOptionsPanel.java b/editor.fold/src/org/netbeans/modules/editor/fold/ui/FoldOptionsPanel.java new file mode 100644 --- /dev/null +++ b/editor.fold/src/org/netbeans/modules/editor/fold/ui/FoldOptionsPanel.java @@ -0,0 +1,424 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2013 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + * + * Contributor(s): + * + * Portions Copyrighted 2013 Sun Microsystems, Inc. + */ +package org.netbeans.modules.editor.fold.ui; + +import java.awt.CardLayout; +import java.awt.Component; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.prefs.PreferenceChangeEvent; +import java.util.prefs.PreferenceChangeListener; +import java.util.prefs.Preferences; +import javax.swing.DefaultComboBoxModel; +import javax.swing.DefaultListCellRenderer; +import javax.swing.JComponent; +import javax.swing.JList; +import org.netbeans.api.editor.fold.FoldUtilities; +import org.netbeans.api.editor.mimelookup.MimePath; +import org.netbeans.api.editor.settings.SimpleValueNames; +import org.netbeans.api.lexer.Language; +import org.netbeans.modules.editor.fold.FoldUtilitiesImpl; +import org.netbeans.modules.editor.settings.storage.api.EditorSettings; +import org.openide.util.NbBundle; +import org.openide.util.WeakListeners; + +import static org.netbeans.modules.editor.fold.ui.Bundle.*; + +/** + * UI for the folding enable + language switch. + * The panel contains a placeholder for language-specific contents. When language is selected, it replaces the + * interior with a language-specific panel. + * + * @author sdedic + */ +final class FoldOptionsPanel extends javax.swing.JPanel implements ActionListener, PreferenceChangeListener { + /** + * All mime types presented in the selector + */ + private List languageMimeTypes; + + /** + * Our controller + */ + private FoldOptionsController ctrl; + + private Preferences parentPrefs; + + /** + * Creates new form FoldOptionsPanel + */ + public FoldOptionsPanel(FoldOptionsController ctrl) { + this.ctrl = ctrl; + initComponents(); + + langSelect.setRenderer(new DefaultListCellRenderer() { + @Override + public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { + if (value instanceof String[]) { + value = ((String[])value)[1]; + } + return super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); //To change body of generated methods, choose Tools | Templates. + } + }); + langSelect.addActionListener(this); + contentPreview.addActionListener(this); + foldedSummary.addActionListener(this); + + // preferences should be set as a reaction to index selection + } + + /** + * The preferences object for the currently selected language + */ + private Preferences currentPreferences; + + /** + * Panels created for individual Mime types + */ + private Map panels = new HashMap(); + + private PreferenceChangeListener wPrefL = WeakListeners.create(PreferenceChangeListener.class, this, null); + + private void languageSelected() { + String[] sel = (String[])langSelect.getSelectedItem(); + if (sel == null) { + return; + } + String mime = sel[0]; + if (currentPreferences != null) { + currentPreferences.removePreferenceChangeListener(wPrefL); + } + currentPreferences = ctrl.prefs(mime); + JComponent panel = panels.get(mime); + String parentMime = MimePath.parse(mime).getInheritedType(); + if (parentMime != null) { + parentPrefs = ctrl.prefs(parentMime); + } else { + parentPrefs = null; + } + if (panel == null) { + panel = new DefaultFoldingOptions(mime, currentPreferences); + if (panel instanceof CustomizerWithDefaults) { + ((CustomizerWithDefaults)panel).setDefaultPreferences(parentPrefs); + } + panels.put(mime, panel); + content.add(panel, mime); + } + ((CardLayout)content.getLayout()).show(content, mime); + currentPreferences.addPreferenceChangeListener(wPrefL); + useDefaults.setVisible(!"".equals(mime)); // NOI18N + preferenceChange(null); + } + + private void previewChanged() { + currentPreferences.putBoolean(FoldUtilitiesImpl.PREF_CONTENT_PREVIEW, contentPreview.isSelected()); + } + + private void summaryChanged() { + currentPreferences.putBoolean(FoldUtilitiesImpl.PREF_CONTENT_SUMMARY, foldedSummary.isSelected()); + } + + @Override + public void actionPerformed(ActionEvent e) { + Object o = e.getSource(); + if (ignoreEnableTrigger) { + return; + } + if (o == langSelect) { + languageSelected(); + } else if (o == contentPreview) { + previewChanged(); + } else if (o == foldedSummary) { + summaryChanged(); + } + } + + @Override + public void preferenceChange(PreferenceChangeEvent evt) { + String k = evt == null ? null : evt.getKey(); + ignoreEnableTrigger = true; + try { + if (k == null || k.equals(FoldUtilitiesImpl.PREF_OVERRIDE_DEFAULTS)) { + useDefaults.setSelected(currentPreferences.getBoolean(FoldUtilitiesImpl.PREF_OVERRIDE_DEFAULTS, true)); + } + if (k == null || k.equals(SimpleValueNames.CODE_FOLDING_ENABLE)) { + boolean enabled = currentPreferences.getBoolean(SimpleValueNames.CODE_FOLDING_ENABLE, true); + enableFolds.setSelected(enabled); + contentPreview.setEnabled(enabled); + foldedSummary.setEnabled(enabled); + useDefaults.setEnabled(enabled); + } + if (k == null || FoldUtilitiesImpl.PREF_CONTENT_PREVIEW.equals(FoldUtilitiesImpl.PREF_CONTENT_PREVIEW)) { + contentPreview.setSelected(currentPreferences.getBoolean(FoldUtilitiesImpl.PREF_CONTENT_PREVIEW, true)); + } + if (k == null || FoldUtilitiesImpl.PREF_CONTENT_SUMMARY.equals(FoldUtilitiesImpl.PREF_CONTENT_SUMMARY)) { + foldedSummary.setSelected(currentPreferences.getBoolean(FoldUtilitiesImpl.PREF_CONTENT_SUMMARY, true)); + } + if (k == null || FoldUtilitiesImpl.PREF_OVERRIDE_DEFAULTS.equals(k)) { + boolean b = parentPrefs == null || !currentPreferences.getBoolean(FoldUtilitiesImpl.PREF_OVERRIDE_DEFAULTS, true); + if (parentPrefs != null) { + if (b) { + currentPreferences.putBoolean(FoldUtilitiesImpl.PREF_CONTENT_PREVIEW, + parentPrefs.getBoolean(FoldUtilitiesImpl.PREF_CONTENT_PREVIEW, true)); + currentPreferences.putBoolean(FoldUtilitiesImpl.PREF_CONTENT_SUMMARY, + parentPrefs.getBoolean(FoldUtilitiesImpl.PREF_CONTENT_SUMMARY, true)); + } else { + currentPreferences.remove(FoldUtilitiesImpl.PREF_CONTENT_PREVIEW); + currentPreferences.remove(FoldUtilitiesImpl.PREF_CONTENT_SUMMARY); + } + } + contentPreview.setEnabled(b); + foldedSummary.setEnabled(b); + contentPreview.setSelected(currentPreferences.getBoolean(FoldUtilitiesImpl.PREF_CONTENT_PREVIEW, true)); + foldedSummary.setSelected(currentPreferences.getBoolean(FoldUtilitiesImpl.PREF_CONTENT_SUMMARY, true)); + } + } finally { + ignoreEnableTrigger = false; + } + } + + void update() { + initialize(); + } + + @NbBundle.Messages({ + "ITEM_AllLanguages=All Languages" + }) + private void initialize() { + Set mimeTypes = EditorSettings.getDefault().getAllMimeTypes(); + List langMimes = new ArrayList(mimeTypes.size()); + langMimes.add(new String[] { "", ITEM_AllLanguages() }); // NOI18N + for (String s : mimeTypes) { + Language l = Language.find(s); + if (l == null) { + continue; + } + // filter out languages, whose author didn't name it - probably + // unimportant && should not be displayed. + String name = EditorSettings.getDefault().getLanguageName(s); + if (name.equals(s)) { + continue; + } + // last, discard everything that does not have any FoldTypes: + if (FoldUtilities.getFoldTypes(s).values().isEmpty()) { + continue; + } + langMimes.add(new String[] { + s, EditorSettings.getDefault().getLanguageName(s) + }); + } + Collections.sort(langMimes, LANG_COMPARATOR); + languageMimeTypes = langMimes; + + langSelect.setModel(new DefaultComboBoxModel(languageMimeTypes.toArray(new Object[languageMimeTypes.size()]))); + langSelect.setSelectedIndex(0); + } + + void clear() { + panels.clear(); + } + + /** + * Special comparator, which sorts "" mime type first, other Mimetypes are then sorted based on the language names, + * alphabetically. It is expected that the 1st member of the String[]is a Mimetype string, the 2nd member is a + * human-readable language name. + */ + private static final Comparator LANG_COMPARATOR = new Comparator() { + @Override + public int compare(String[] o1, String[] o2) { + if (o1 == null) { + return -1; + } else if (o2 == null) { + return 1; + } + if (o1[0].equals(o2[0])) { + return 0; + } + // sort 'all languages' first + if (o1[0].length() == 0) { + return -1; + } else if (o2[0].length() == 0) { + return 1; + } + return o1[1].compareToIgnoreCase(o2[1]); + } + }; + + private boolean ignoreEnableTrigger; + + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + @SuppressWarnings("unchecked") + // //GEN-BEGIN:initComponents + private void initComponents() { + + langSelect = new javax.swing.JComboBox(); + langLabel = new javax.swing.JLabel(); + content = new javax.swing.JPanel(); + enableFolds = new javax.swing.JCheckBox(); + jPanel1 = new javax.swing.JPanel(); + contentPreview = new javax.swing.JCheckBox(); + foldedSummary = new javax.swing.JCheckBox(); + useDefaults = new javax.swing.JCheckBox(); + + org.openide.awt.Mnemonics.setLocalizedText(langLabel, org.openide.util.NbBundle.getMessage(FoldOptionsPanel.class, "FoldOptionsPanel.langLabel.text")); // NOI18N + + content.setLayout(new java.awt.CardLayout()); + + org.openide.awt.Mnemonics.setLocalizedText(enableFolds, org.openide.util.NbBundle.getMessage(FoldOptionsPanel.class, "FoldOptionsPanel.enableFolds.text")); // NOI18N + enableFolds.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + enableFoldsActionPerformed(evt); + } + }); + + jPanel1.setBorder(javax.swing.BorderFactory.createTitledBorder(org.openide.util.NbBundle.getMessage(FoldOptionsPanel.class, "Title_FoldDisplayOptions"))); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(contentPreview, org.openide.util.NbBundle.getMessage(FoldOptionsPanel.class, "FoldOptionsPanel.contentPreview.text")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(foldedSummary, org.openide.util.NbBundle.getMessage(FoldOptionsPanel.class, "FoldOptionsPanel.foldedSummary.text")); // NOI18N + + javax.swing.GroupLayout jPanel1Layout = new javax.swing.GroupLayout(jPanel1); + jPanel1.setLayout(jPanel1Layout); + jPanel1Layout.setHorizontalGroup( + jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(jPanel1Layout.createSequentialGroup() + .addContainerGap() + .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(contentPreview) + .addComponent(foldedSummary)) + .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + ); + jPanel1Layout.setVerticalGroup( + jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(jPanel1Layout.createSequentialGroup() + .addContainerGap() + .addComponent(contentPreview) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(foldedSummary) + .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + ); + + org.openide.awt.Mnemonics.setLocalizedText(useDefaults, org.openide.util.NbBundle.getMessage(FoldOptionsPanel.class, "FoldOptionsPanel.useDefaults.text")); // NOI18N + useDefaults.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + useDefaultsActionPerformed(evt); + } + }); + + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); + this.setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(content, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(jPanel1, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addGroup(layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(useDefaults) + .addGroup(layout.createSequentialGroup() + .addComponent(langLabel) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(langSelect, javax.swing.GroupLayout.PREFERRED_SIZE, 186, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(enableFolds))) + .addGap(0, 0, Short.MAX_VALUE))) + .addContainerGap()) + ); + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(langSelect, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(langLabel) + .addComponent(enableFolds)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(useDefaults) + .addGap(12, 12, 12) + .addComponent(content, javax.swing.GroupLayout.DEFAULT_SIZE, 25, Short.MAX_VALUE) + .addGap(18, 18, 18) + .addComponent(jPanel1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addContainerGap()) + ); + }// //GEN-END:initComponents + + private void enableFoldsActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_enableFoldsActionPerformed + if (ignoreEnableTrigger) { + return; + } + boolean enable = enableFolds.isSelected(); + currentPreferences.putBoolean(SimpleValueNames.CODE_FOLDING_ENABLE, enable); + // visual feedback handled by listener. + }//GEN-LAST:event_enableFoldsActionPerformed + + private void useDefaultsActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_useDefaultsActionPerformed + if (ignoreEnableTrigger) { + return; + } + currentPreferences.putBoolean(FoldUtilitiesImpl.PREF_OVERRIDE_DEFAULTS, + useDefaults.isSelected()); + }//GEN-LAST:event_useDefaultsActionPerformed + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JPanel content; + private javax.swing.JCheckBox contentPreview; + private javax.swing.JCheckBox enableFolds; + private javax.swing.JCheckBox foldedSummary; + private javax.swing.JPanel jPanel1; + private javax.swing.JLabel langLabel; + private javax.swing.JComboBox langSelect; + private javax.swing.JCheckBox useDefaults; + // End of variables declaration//GEN-END:variables +} diff --git a/editor.fold/src/org/netbeans/modules/editor/fold/ui/FoldToolTip.java b/editor.fold/src/org/netbeans/modules/editor/fold/ui/FoldToolTip.java new file mode 100644 --- /dev/null +++ b/editor.fold/src/org/netbeans/modules/editor/fold/ui/FoldToolTip.java @@ -0,0 +1,152 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 1997-2010 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * Contributor(s): + * + * The Original Software is NetBeans. The Initial Developer of the Original + * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun + * Microsystems, Inc. All Rights Reserved. + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + */ + +package org.netbeans.modules.editor.fold.ui; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Dimension; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import javax.swing.JComponent; +import javax.swing.JEditorPane; +import javax.swing.JPanel; +import javax.swing.border.LineBorder; +import javax.swing.event.AncestorEvent; +import javax.swing.event.AncestorListener; +import javax.swing.text.JTextComponent; +import org.netbeans.modules.editor.lib2.view.DocumentView; +import org.openide.util.Exceptions; +import org.openide.util.Lookup; + +/** + * Component that displays a collapsed fold preview. + * + * @author Miloslav Metelka + */ +final class FoldToolTip extends JPanel { + private int editorPaneWidth; + + public FoldToolTip(JEditorPane editorPane, final JEditorPane foldPreviewPane, Color borderColor) { + setLayout(new BorderLayout()); + add(foldPreviewPane, BorderLayout.CENTER); + putClientProperty("tooltip-type", "fold-preview"); // Checked in NbToolTip + + addGlyphGutter(foldPreviewPane); + + addAncestorListener(new AncestorListener() { + @Override + public void ancestorAdded(AncestorEvent event) { + } + + @Override + public void ancestorRemoved(AncestorEvent event) { + // Deactivate the view hierarchy immediately for foldPreviewPane + final DocumentView docView = DocumentView.get(foldPreviewPane); + if (docView != null) { + docView.runTransaction(new Runnable() { + @Override + public void run() { + docView.updateLengthyAtomicEdit(+100); // Effectively disable any VH updates + } + }); + } + // Remove the listener + FoldToolTip.this.removeAncestorListener(this); + } + + @Override + public void ancestorMoved(AncestorEvent event) { + } + }); + + editorPaneWidth = editorPane.getSize().width; + + setBorder(new LineBorder(borderColor)); + setOpaque(true); + } + + private void addGlyphGutter(JTextComponent jtx) { + ClassLoader cls = Lookup.getDefault().lookup(ClassLoader.class); + Class clazz; + Class editorUiClass; + + JComponent gutter = null; + try { + clazz = Class.forName("org.netbeans.editor.GlyphGutter", true, cls); // NOI18N + editorUiClass = Class.forName("org.netbeans.editor.EditorUI", true, cls); // NOI18N + // get the factory instance + Object o = clazz.newInstance(); + Method m = clazz.getDeclaredMethod("createSideBar", JTextComponent.class); // NOI18N + gutter = (JComponent)m.invoke(o, jtx); + } catch (IllegalArgumentException ex) { + Exceptions.printStackTrace(ex); + } catch (InvocationTargetException ex) { + Exceptions.printStackTrace(ex); + } catch (NoSuchMethodException ex) { + Exceptions.printStackTrace(ex); + } catch (SecurityException ex) { + Exceptions.printStackTrace(ex); + } catch (InstantiationException ex) { + Exceptions.printStackTrace(ex); + } catch (IllegalAccessException ex) { + Exceptions.printStackTrace(ex); + } catch (ClassNotFoundException ex) { + Exceptions.printStackTrace(ex); + } + if (gutter != null) { + add(gutter, BorderLayout.WEST); + } + } + + @Override + public Dimension getPreferredSize() { + Dimension prefSize = super.getPreferredSize(); + // Return width like for editor pane which forces the PopupManager to display + // the tooltip to align exacty with the text (below/above). + prefSize.width = Math.min(prefSize.width, editorPaneWidth); + return prefSize; + } + +} diff --git a/editor.fold/src/org/netbeans/modules/editor/fold/ui/FoldView.java b/editor.fold/src/org/netbeans/modules/editor/fold/ui/FoldView.java new file mode 100644 --- /dev/null +++ b/editor.fold/src/org/netbeans/modules/editor/fold/ui/FoldView.java @@ -0,0 +1,430 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 1997-2010 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * Contributor(s): + * + * The Original Software is NetBeans. The Initial Developer of the Original + * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun + * Microsystems, Inc. All Rights Reserved. + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + */ + +package org.netbeans.modules.editor.fold.ui; + +import org.netbeans.modules.editor.fold.ui.FoldViewFactory; +import java.awt.Color; +import java.awt.Container; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.Rectangle; +import java.awt.Shape; +import java.awt.font.FontRenderContext; +import java.awt.font.TextHitInfo; +import java.awt.font.TextLayout; +import java.awt.geom.Rectangle2D; +import java.util.logging.Logger; +import javax.swing.JComponent; +import javax.swing.JEditorPane; +import javax.swing.text.AttributeSet; +import javax.swing.text.BadLocationException; +import javax.swing.text.Document; +import javax.swing.text.EditorKit; +import javax.swing.text.Element; +import javax.swing.text.JTextComponent; +import javax.swing.text.Position; +import javax.swing.text.Position.Bias; +import javax.swing.text.StyleConstants; +import javax.swing.text.View; +import org.netbeans.api.editor.fold.Fold; +import org.netbeans.api.editor.fold.FoldTemplate; +import org.netbeans.api.editor.settings.FontColorNames; +import org.netbeans.api.editor.settings.FontColorSettings; +import org.netbeans.editor.BaseDocument; +import org.netbeans.editor.Utilities; +import org.netbeans.modules.editor.fold.FoldContentReaders; +import org.netbeans.modules.editor.lib2.view.EditorView; +import org.netbeans.modules.editor.lib2.view.ViewRenderContext; +import org.netbeans.modules.editor.lib2.view.ViewUtils; +import org.openide.util.NbBundle; + +import static org.netbeans.modules.editor.fold.ui.Bundle.*; + + +/** + * View with highlights. This is the most used view. + * + * @author Miloslav Metelka + */ + +final class FoldView extends EditorView { + + // -J-Dorg.netbeans.modules.editor.lib2.view.HighlightsView.level=FINE + private static final Logger LOG = Logger.getLogger(FoldView.class.getName()); + + /** + * Extra space added to each side of description text of a fold view. + */ + private static final float EXTRA_MARGIN_WIDTH = 3; + + /** Raw end offset of this view. */ + private int rawEndOffset; // 24-super + 4 = 28 bytes + + /** Length of text occupied by this view. */ + private int length; // 28 + 4 = 32 bytes + + private final JTextComponent textComponent; // 32 + 4 = 36 bytes + + private final Fold fold; // 36 + 4 = 40 bytes + + private TextLayout collapsedTextLayout; // 40 + 4 = 44 bytes + + private AttributeSet foldingColors; + + private int options; + + public FoldView(JTextComponent textComponent, Fold fold, FontColorSettings colorSettings, int options) { + super(null); + int offset = fold.getStartOffset(); + int len = fold.getEndOffset() - offset; + assert (len > 0) : "length=" + len + " <= 0"; // NOI18N + this.length = len; + this.textComponent = textComponent; + this.fold = fold; + this.foldingColors = colorSettings.getFontColors(FontColorNames.CODE_FOLDING_COLORING); + this.options = options; + } + + @Override + public float getPreferredSpan(int axis) { + if (axis == View.X_AXIS) { + String desc = fold.getDescription(); // For empty desc a single-space text layout is returned + float advance = 0; + if (desc.length() > 0) { + TextLayout textLayout = getTextLayout(); + if (textLayout == null) { + return 0f; + } + advance = textLayout.getAdvance(); + } + return advance + (2 * EXTRA_MARGIN_WIDTH); + } else { + EditorView.Parent parent = (EditorView.Parent) getParent(); + return (parent != null) ? parent.getViewRenderContext().getDefaultRowHeight() : 0f; + } + } + + @Override + public int getRawEndOffset() { + return rawEndOffset; + } + + @Override + public void setRawEndOffset(int rawOffset) { + this.rawEndOffset = rawOffset; + } + + @Override + public int getLength() { + return length; + } + + @Override + public int getStartOffset() { + return getEndOffset() - getLength(); + } + + @Override + public int getEndOffset() { + EditorView.Parent parent = (EditorView.Parent) getParent(); + return (parent != null) ? parent.getViewEndOffset(rawEndOffset) : rawEndOffset; + } + + @Override + public Document getDocument() { + View parent = getParent(); + return (parent != null) ? parent.getDocument() : null; + } + + @Override + public AttributeSet getAttributes() { + return null; + } + + @NbBundle.Messages({ + "# {0} - number of lines", + "FMT_contentSummary={0} line(s)" + }) + private String resolvePlaceholder(String text, int at) { + if ((options & 3) == 0) { + return text; + } + Document d = getDocument(); + if (!(d instanceof BaseDocument)) { + return null; + } + BaseDocument bd = (BaseDocument)d; + CharSequence contentSeq = ""; // NOI18N + String summary = ""; // NOI18N + + int mask = options; + try { + if ((options & 1) > 0) { + contentSeq = FoldContentReaders.get().readContent( + org.netbeans.lib.editor.util.swing.DocumentUtilities.getMimeType(textComponent), + d, + fold, + fold.getType().getTemplate()); + if (contentSeq == null) { + mask &= ~1; + } + } + if ((options & 2) > 0) { + int start = fold.getStartOffset(); + int end = fold.getEndOffset(); + int startLine = Utilities.getLineOffset(bd, start); + int endLine = Utilities.getLineOffset(bd, end) + 1; + + if (endLine <= startLine + 1) { + mask &= ~2; + } else { + summary = FMT_contentSummary((endLine - startLine)); + } + } + } catch (BadLocationException ex) { + } + if (mask == 0) { + return text; + } + String replacement = NbBundle.getMessage(FoldView.class, "FMT_ContentPlaceholder_" + (mask & 3), contentSeq, summary); + StringBuilder sb = new StringBuilder(text.length() + replacement.length()); + sb.append(text.subSequence(0, at)); + sb.append(replacement); + sb.append(text.subSequence(at + FoldTemplate.CONTENT_PLACEHOLDER.length(), text.length())); + return sb.toString(); + } + + private TextLayout getTextLayout() { + if (collapsedTextLayout == null) { + EditorView.Parent parent = (EditorView.Parent) getParent(); + ViewRenderContext context = parent.getViewRenderContext(); + FontRenderContext frc = context.getFontRenderContext(); + assert (frc != null) : "Null FontRenderContext"; // NOI18N + Font font = context.getRenderFont(textComponent.getFont()); + String text = fold.getDescription(); + if (text.length() == 0) { + text = " "; // Use single space (mainly for height measurement etc. + } + int placeIndex = text.indexOf(FoldTemplate.CONTENT_PLACEHOLDER); + if (placeIndex > -1) { + text = resolvePlaceholder(text, placeIndex); + } + collapsedTextLayout = new TextLayout(text, font, frc); + } + return collapsedTextLayout; + } + + @Override + public Shape modelToViewChecked(int offset, Shape alloc, Position.Bias bias) { +// TextLayout textLayout = getTextLayout(); +// if (textLayout == null) { +// return alloc; // Leave given bounds +// } +// Rectangle2D.Double bounds = ViewUtils.shape2Bounds(alloc); +// return bounds; + return alloc; + } + + @Override + public int viewToModelChecked(double x, double y, Shape alloc, Position.Bias[] biasReturn) { + int startOffset = getStartOffset(); + return startOffset; + } + + static TextHitInfo x2RelOffset(TextLayout textLayout, float x) { + TextHitInfo hit; + x -= EXTRA_MARGIN_WIDTH; + if (x >= textLayout.getAdvance()) { + hit = TextHitInfo.trailing(textLayout.getCharacterCount()); + } else { + hit = textLayout.hitTestChar(x, 0); // What about backward bias -> with higher offsets it may go back visually + } + return hit; + + } + + @Override + public int getNextVisualPositionFromChecked(int offset, Bias bias, Shape alloc, int direction, Bias[] biasRet) { + int startOffset = getStartOffset(); + int retOffset = -1; + switch (direction) { + case WEST: + if (offset == -1) { + retOffset = startOffset; + } else { + retOffset = -1; + } + break; + + case EAST: + if (offset == -1) { + retOffset = startOffset; + } else { + retOffset = -1; + } + break; + + case NORTH: + case SOUTH: + break; + default: + throw new IllegalArgumentException("Bad direction: " + direction); + } + return retOffset; + } + + @Override + public JComponent getToolTip(double x, double y, Shape allocation) { + Container container = getContainer(); + if (container instanceof JEditorPane) { + JEditorPane editorPane = (JEditorPane) getContainer(); + JEditorPane tooltipPane = new JEditorPane(); + EditorKit kit = editorPane.getEditorKit(); + Document doc = getDocument(); + if (kit != null && doc != null) { + Element lineRootElement = doc.getDefaultRootElement(); + tooltipPane.putClientProperty(FoldViewFactory.VIEW_FOLDS_EXPANDED_PROPERTY, true); + try { + // Start-offset of the fold => line start => position + int lineIndex = lineRootElement.getElementIndex(fold.getStartOffset()); + Position pos = doc.createPosition( + lineRootElement.getElement(lineIndex).getStartOffset()); + // DocumentView.START_POSITION_PROPERTY + tooltipPane.putClientProperty("document-view-start-position", pos); + // End-offset of the fold => line end => position + lineIndex = lineRootElement.getElementIndex(fold.getEndOffset()); + pos = doc.createPosition(lineRootElement.getElement(lineIndex).getEndOffset()); + // DocumentView.END_POSITION_PROPERTY + tooltipPane.putClientProperty("document-view-end-position", pos); + tooltipPane.putClientProperty("document-view-accurate-span", true); + // Set the same kit and document + tooltipPane.setEditorKit(kit); + tooltipPane.setDocument(doc); + tooltipPane.setEditable(false); + return new FoldToolTip(editorPane, tooltipPane, getForegroundColor()); + } catch (BadLocationException e) { + // => return null + } + } + } + return null; + } + + private Color getForegroundColor() { + if (foldingColors == null) { + return textComponent.getForeground(); + } + Object bgColorObj = foldingColors.getAttribute(StyleConstants.Foreground); + if (bgColorObj instanceof Color) { + return (Color)bgColorObj; + } else { + return textComponent.getForeground(); + } + } + + private Color getBackgroundColor() { + if (foldingColors == null) { + return textComponent.getBackground(); + } + Object bgColorObj = foldingColors.getAttribute(StyleConstants.Background); + if (bgColorObj instanceof Color) { + return (Color)bgColorObj; + } else { + return textComponent.getBackground(); + } + } + + @Override + public void paint(Graphics2D g, Shape alloc, Rectangle clipBounds) { + Rectangle2D.Double allocBounds = ViewUtils.shape2Bounds(alloc); + if (allocBounds.intersects(clipBounds)) { + Font origFont = g.getFont(); + Color origColor = g.getColor(); + Color origBkColor = g.getBackground(); + Shape origClip = g.getClip(); + try { + // Leave component font + g.setColor(getForegroundColor()); + g.setBackground(getBackgroundColor()); + + int xInt = (int) allocBounds.getX(); + int yInt = (int) allocBounds.getY(); + int endXInt = (int) (allocBounds.getX() + allocBounds.getWidth() - 1); + int endYInt = (int) (allocBounds.getY() + allocBounds.getHeight() - 1); + g.drawRect(xInt, yInt, endXInt - xInt, endYInt - yInt); + g.clearRect(xInt + 1, yInt + 1, endXInt - xInt - 1, endYInt - yInt - 1); + g.clip(alloc); + TextLayout textLayout = getTextLayout(); + if (textLayout != null) { + EditorView.Parent parent = (EditorView.Parent) getParent(); + float ascent = parent.getViewRenderContext().getDefaultAscent(); + String desc = fold.getDescription(); // For empty desc a single-space text layout is returned + float x = (float) (allocBounds.getX() + EXTRA_MARGIN_WIDTH); + float y = (float) allocBounds.getY(); + if (desc.length() > 0) { + + textLayout.draw(g, x, y + ascent); + } + } + } finally { + g.setClip(origClip); + g.setBackground(origBkColor); + g.setColor(origColor); + g.setFont(origFont); + } + } + } + + @Override + protected String getDumpName() { + return "FV"; + } + + @Override + public String toString() { + return appendViewInfo(new StringBuilder(200), 0, "", -1).toString(); + } + +} diff --git a/editor.fold/src/org/netbeans/modules/editor/fold/ui/FoldViewFactory.java b/editor.fold/src/org/netbeans/modules/editor/fold/ui/FoldViewFactory.java new file mode 100644 --- /dev/null +++ b/editor.fold/src/org/netbeans/modules/editor/fold/ui/FoldViewFactory.java @@ -0,0 +1,272 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 1997-2010 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * Contributor(s): + * + * The Original Software is NetBeans. The Initial Developer of the Original + * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun + * Microsystems, Inc. All Rights Reserved. + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + */ + +package org.netbeans.modules.editor.fold.ui; + +import java.util.Iterator; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.prefs.PreferenceChangeEvent; +import java.util.prefs.PreferenceChangeListener; +import java.util.prefs.Preferences; +import javax.swing.text.View; +import org.netbeans.api.editor.fold.Fold; +import org.netbeans.api.editor.fold.FoldHierarchy; +import org.netbeans.api.editor.fold.FoldHierarchyEvent; +import org.netbeans.api.editor.fold.FoldHierarchyListener; +import org.netbeans.api.editor.fold.FoldUtilities; +import org.netbeans.api.editor.mimelookup.MimeLookup; +import org.netbeans.api.editor.settings.FontColorSettings; +import org.netbeans.lib.editor.util.swing.DocumentUtilities; +import org.netbeans.modules.editor.fold.FoldUtilitiesImpl; +import org.netbeans.modules.editor.lib2.view.EditorView; +import org.netbeans.modules.editor.lib2.view.EditorViewFactory; +import org.netbeans.modules.editor.lib2.view.EditorViewFactoryChange; +import org.netbeans.modules.editor.lib2.view.ViewUtils; +import org.openide.util.Lookup; +import org.openide.util.LookupEvent; +import org.openide.util.LookupListener; +import org.openide.util.WeakListeners; + +/** + * View factory creating views for collapsed folds. + * + * @author Miloslav Metelka + */ + +@SuppressWarnings("ClassWithMultipleLoggers") +public final class FoldViewFactory extends EditorViewFactory implements FoldHierarchyListener, LookupListener, PreferenceChangeListener { + + /** + * Component's client property which can be set to view folds expanded for tooltip fold preview. + */ + static final String VIEW_FOLDS_EXPANDED_PROPERTY = "view-folds-expanded"; // NOI18N + + // -J-Dorg.netbeans.editor.view.change.level=FINE + static final Logger CHANGE_LOG = Logger.getLogger("org.netbeans.editor.view.change"); + + // -J-Dorg.netbeans.modules.editor.fold.FoldViewFactory.level=FINE + private static final Logger LOG = Logger.getLogger(FoldViewFactory.class.getName()); + + public static void register() { + EditorViewFactory.registerFactory(new FoldFactory()); + } + + private FoldHierarchy foldHierarchy; + + private boolean foldHierarchyLocked; + + private Fold fold; + + private int foldStartOffset; + + private Iterator collapsedFoldIterator; + + private boolean viewFoldsExpanded; + + /** + * Composite Color settings from MIME lookup + */ + private FontColorSettings colorSettings; + + /** + * Lookup results for color settings, being listened for changes. + */ + private Lookup.Result colorSource; + + private Preferences prefs; + + private int viewFlags = 0; + + public FoldViewFactory(View documentView) { + super(documentView); + foldHierarchy = FoldHierarchy.get(textComponent()); + foldHierarchy.addFoldHierarchyListener(this); + viewFoldsExpanded = Boolean.TRUE.equals(textComponent().getClientProperty(VIEW_FOLDS_EXPANDED_PROPERTY)); + + String mime = DocumentUtilities.getMimeType(document()); + + Lookup lkp = MimeLookup.getLookup(mime); + colorSource = lkp.lookupResult(FontColorSettings.class); + colorSource.addLookupListener(WeakListeners.create(LookupListener.class, this, colorSource)); + colorSettings = (FontColorSettings)colorSource.allInstances().iterator().next(); + prefs = lkp.lookup(Preferences.class); + prefs.addPreferenceChangeListener(WeakListeners.create(PreferenceChangeListener.class, this, prefs)); + + initViewFlags(); + } + + private void initViewFlags() { + viewFlags = + (prefs.getBoolean(FoldUtilitiesImpl.PREF_CONTENT_PREVIEW, true) ? 1 : 0) | + (prefs.getBoolean(FoldUtilitiesImpl.PREF_CONTENT_SUMMARY, true) ? 2 : 0); + } + + @Override + public void resultChanged(LookupEvent ev) { + refreshColors(); + } + + @Override + public void preferenceChange(PreferenceChangeEvent evt) { + String k = evt.getKey(); + if (FoldUtilitiesImpl.PREF_CONTENT_PREVIEW.equals(k) || + FoldUtilitiesImpl.PREF_CONTENT_SUMMARY.equals(k)) { + initViewFlags(); + document().render(new Runnable() { + @Override + public void run() { + int end = document().getLength(); + fireEvent(EditorViewFactoryChange.createList(0, end, EditorViewFactoryChange.Type.CHARACTER_CHANGE)); + } + }); + } + } + + private void refreshColors() { + colorSettings = (FontColorSettings)colorSource.allInstances().iterator().next(); + document().render(new Runnable() { + @Override + public void run() { + int end = document().getLength(); + fireEvent(EditorViewFactoryChange.createList(0, end, EditorViewFactoryChange.Type.CHARACTER_CHANGE)); + } + }); + } + + @Override + public void restart(int startOffset, int endOffset, boolean createViews) { + foldHierarchy.lock(); // this.finish() always called in try-finally + foldHierarchyLocked = true; + @SuppressWarnings("unchecked") + Iterator it = FoldUtilities.collapsedFoldIterator(foldHierarchy, startOffset, Integer.MAX_VALUE); + collapsedFoldIterator = it; + foldStartOffset = -1; // Make a next call to updateFold() to fetch a fold + } + + private void updateFold(int offset) { + if (foldStartOffset < offset) { + while (collapsedFoldIterator.hasNext()) { + fold = collapsedFoldIterator.next(); + foldStartOffset = fold.getStartOffset(); + if (foldStartOffset >= offset) { + return; + } + } + fold = null; + foldStartOffset = Integer.MAX_VALUE; + } + } + + @Override + public int nextViewStartOffset(int offset) { + if (!viewFoldsExpanded) { + updateFold(offset); + return foldStartOffset; + } + return Integer.MAX_VALUE; + } + + @Override + public EditorView createView(int startOffset, int limitOffset, boolean forcedLimit, + EditorView origView, int nextOrigViewOffset) { + assert (startOffset == foldStartOffset) : "startOffset=" + startOffset + " != foldStartOffset=" + foldStartOffset; // NOI18N + if (fold.getEndOffset() <= limitOffset || !forcedLimit) { + return new FoldView(textComponent(), fold, colorSettings, viewFlags); + } else { + return null; + } + } + + @Override + public int viewEndOffset(int startOffset, int limitOffset, boolean forcedLimit) { + int foldEndOffset = fold.getEndOffset(); + if (foldEndOffset <= limitOffset) { + return foldEndOffset; + } else { + return -1; + } + } + + @Override + public void continueCreation(int startOffset, int endOffset) { + } + + @Override + public void finishCreation() { + fold = null; + collapsedFoldIterator = null; + if (foldHierarchyLocked) { + foldHierarchy.unlock(); + } + } + + @Override + public void foldHierarchyChanged(FoldHierarchyEvent evt) { + // For fold state changes use a higher priority + int startOffset = evt.getAffectedStartOffset(); + int endOffset = evt.getAffectedEndOffset(); + if (CHANGE_LOG.isLoggable(Level.FINE)) { + ViewUtils.log(CHANGE_LOG, "CHANGE in FoldViewFactory: <" + // NOI18N + startOffset + "," + endOffset + ">\n"); // NOI18N + } + fireEvent(EditorViewFactoryChange.createList(startOffset, endOffset, + EditorViewFactoryChange.Type.PARAGRAPH_CHANGE)); + } + + public static final class FoldFactory implements EditorViewFactory.Factory { + + @Override + public EditorViewFactory createEditorViewFactory(View documentView) { + return new FoldViewFactory(documentView); + } + + @Override + public int weight() { + return 100; + } + + } + +} diff --git a/editor.fold/src/org/netbeans/modules/editor/fold/ui/InheritedPreferences.java b/editor.fold/src/org/netbeans/modules/editor/fold/ui/InheritedPreferences.java new file mode 100644 --- /dev/null +++ b/editor.fold/src/org/netbeans/modules/editor/fold/ui/InheritedPreferences.java @@ -0,0 +1,252 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2013 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + * + * Contributor(s): + * + * Portions Copyrighted 2013 Sun Microsystems, Inc. + */ +package org.netbeans.modules.editor.fold.ui; + +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.prefs.AbstractPreferences; +import java.util.prefs.BackingStoreException; +import java.util.prefs.PreferenceChangeEvent; +import java.util.prefs.PreferenceChangeListener; +import java.util.prefs.Preferences; +import org.netbeans.modules.editor.settings.storage.api.OverridePreferences; + +/** + * Support for inheriting Preferences, while still working with stored ones. + * + * This class solves the 'diamond inheritance', which is present during editing: + * a MIME-type preferences derive from BOTH its persistent values (preferred) and + * from the parent, whose actual values are potentially transient, and also persistent. + *

+ * Let us assume the following assignment: + *

    + *
  • TC (this current) = currently added/changed/removed values + *
  • TP (this persistent) = persistent values, the getLocal() part of the Mime PreferencesImpl object + *
  • PC (parent current) = currently added/changed/removed values of the parent + *
  • PP (parent persistent) = persistent values, the getLocal() part of the parent MimePreferences + *
+ * The desired priority to find a value is: TC, TP, PC, PP. Because of {@link MemoryPreferences}, the + * PC, PP (and potentially fallback to a grandparent) we already have, if we use the parent's {@link MemoryPreferences} + * preferences as 'inherited'. The "TC" is handled by ProxyPreferences for this Mime node. In InheritedPreferences, + * we must only inject the TP in between TC and the parent's preferences (PC, PP, ...) + *

+ * The object is intended to act as a ProxyPreferences delegate, all writes go directly to the stored + * Mime preferences. + * + * @author sdedic + */ +public final class InheritedPreferences extends AbstractPreferences implements PreferenceChangeListener, OverridePreferences { + /** + * Preferences inherited, ie from a parent Mime type + */ + private Preferences inherited; + + /** + * Stored preferences, + */ + private Preferences stored; + + public InheritedPreferences(Preferences inherited, Preferences stored) { + super(null, ""); // NOI18N + this.inherited = inherited; + if (!(stored instanceof OverridePreferences)) { + throw new IllegalArgumentException(); + } + this.stored = stored; + + inherited.addPreferenceChangeListener(this); + } + + @Override + protected void putSpi(String key, String value) { + // do nothing, the AbstractPref then just fires an event + } + + @Override + public void put(String key, String value) { + if (Boolean.TRUE != ignorePut.get()) { + stored.put(key, value); + } + super.put(key, value); + } + + @Override + public void putInt(String key, int value) { + if (Boolean.TRUE != ignorePut.get()) { + stored.putInt(key, value); + } + super.putInt(key, value); + } + + @Override + public void putLong(String key, long value) { + if (Boolean.TRUE != ignorePut.get()) { + stored.putLong(key, value); + } + super.putLong(key, value); + } + + @Override + public void putBoolean(String key, boolean value) { + if (Boolean.TRUE != ignorePut.get()) { + stored.putBoolean(key, value); + } + super.putBoolean(key, value); + } + + @Override + public void putFloat(String key, float value) { + if (Boolean.TRUE != ignorePut.get()) { + stored.putFloat(key, value); + } + super.putFloat(key, value); + } + + @Override + public void putDouble(String key, double value) { + if (Boolean.TRUE != ignorePut.get()) { + stored.putDouble(key, value); + } + super.putDouble(key, value); + } + + @Override + public void putByteArray(String key, byte[] value) { + if (Boolean.TRUE != ignorePut.get()) { + stored.putByteArray(key, value); + } + super.putByteArray(key, value); + } + + private ThreadLocal ignorePut = new ThreadLocal(); + + @Override + public void preferenceChange(PreferenceChangeEvent evt) { + if (!isOverriden(evt.getKey())) { + // jusr refires an event + ignorePut.set(true); + try { + put(evt.getKey(), evt.getNewValue()); + } finally { + ignorePut.set(false); + } + } + } + + /** + * The value is defined locally, if the stored prefs define the value + * locally. The parent definitions do not count. It is expected, that the + * ProxyPreferences will report its local overrides as local in front of this + * InheritedPreferences. + * + * @param k + * @return + */ + public @Override boolean isOverriden(String k) { + if (stored instanceof OverridePreferences) { + return ((OverridePreferences)stored).isOverriden(k); + } else { + return true; + } + } + + @Override + protected String getSpi(String key) { + // check the stored values + OverridePreferences localStored = (OverridePreferences)stored; + if (localStored.isOverriden(key)) { + return stored.get(key, null); + } + // fall back to the inherited prefs, potentially its stored values etc. + return inherited.get(key, null); + } + + @Override + protected void removeSpi(String key) { + stored.remove(key); + } + + @Override + protected void removeNodeSpi() throws BackingStoreException { + stored.removeNode(); + } + + @Override + protected String[] keysSpi() throws BackingStoreException { + Collection names = new HashSet(); + names.addAll(Arrays.asList(stored.keys())); + names.addAll(Arrays.asList(inherited.keys())); + return names.toArray(new String[names.size()]); + } + + @Override + protected String[] childrenNamesSpi() throws BackingStoreException { + if (stored != null) { + return stored.childrenNames(); + } else { + return new String[0]; + } + } + + @Override + protected AbstractPreferences childSpi(String name) { + Preferences storedNode = stored != null ? stored.node(name) : null; + if (storedNode != null) { + return new InheritedPreferences(null, storedNode); + } else { + return null; + } + } + + @Override + protected void syncSpi() throws BackingStoreException { + stored.sync(); + } + + @Override + protected void flushSpi() throws BackingStoreException { + stored.flush(); + } + + +} diff --git a/editor.fold/src/org/netbeans/modules/editor/fold/ui/VerticalFlowLayout.java b/editor.fold/src/org/netbeans/modules/editor/fold/ui/VerticalFlowLayout.java new file mode 100644 --- /dev/null +++ b/editor.fold/src/org/netbeans/modules/editor/fold/ui/VerticalFlowLayout.java @@ -0,0 +1,175 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2013 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + * + * Contributor(s): + * + * Portions Copyrighted 2013 Sun Microsystems, Inc. + */ +package org.netbeans.modules.editor.fold.ui; + +import java.awt.Component; +import java.awt.Container; +import java.awt.Dimension; +import java.awt.Insets; +import java.awt.LayoutManager2; +import java.util.LinkedHashSet; +import java.util.Set; + +/** + * Simple layout, which fills the space vertically, then overflows to the next column. + * + * @author sdedic + */ +final class VerticalFlowLayout implements LayoutManager2 { + + final private Set components = new LinkedHashSet(); + private int hgap = 0; + private int vgap = 0; + + public void setHGap(int hgap) { + this.hgap = hgap; + } + + public void setVGap(int vgap) { + this.vgap = vgap; + } + + @Override + public void addLayoutComponent(Component comp, Object constraints) { + this.components.add(comp); + } + + @Override + public float getLayoutAlignmentX(Container target) { + return 0; + } + + @Override + public float getLayoutAlignmentY(Container target) { + return 0; + } + + @Override + public void invalidateLayout(Container target) { + } + + @Override + public void addLayoutComponent(String name, Component comp) { + this.components.add(comp); + } + + private Dimension computeDimension(Container parent, int type) { + Insets insets = parent.getInsets(); + int x = insets.left; + int y = insets.top; + int columnWidth = 0; + // int limitHeight = parent.getHeight() - insets.bottom; + int maxY = 0; + + for (Component c : this.components) { + if (c.isVisible()) { + Dimension d; + + switch (type) { + case 0: + d = c.getPreferredSize(); + break; + case 1: + d = c.getMinimumSize(); + break; + default: + d = c.getMaximumSize(); + break; + } + columnWidth = Math.max(columnWidth, d.width); + /* + if (limitHeight != 0 && y + d.height >= limitHeight) { + x += columnWidth + this.hgap; + y = insets.top; + columnWidth = d.width; + } + */ + y += d.height; + maxY = Math.max(y, maxY); + y += this.vgap; + } + } + x += columnWidth; + return new Dimension(x, maxY); + } + + @Override + public void layoutContainer(Container parent) { + Insets insets = parent.getInsets(); + int x = insets.left; + int y = insets.top; + int columnWidth = 0; + int limitHeight = parent.getHeight() - insets.bottom; + for (Component c : this.components) { + if (c.isVisible()) { + Dimension d = c.getPreferredSize(); + columnWidth = Math.max(columnWidth, d.width); + if (y + d.height >= limitHeight) { + x += columnWidth + this.hgap; + y = insets.top; + } + c.setBounds(x, y, d.width, d.height); + y += d.height + this.vgap; + } + } + } + + @Override + public Dimension minimumLayoutSize(Container parent) { + return computeDimension(parent, 1); + } + + @Override + public Dimension preferredLayoutSize(Container parent) { + return computeDimension(parent, 1); + } + + @Override + public Dimension maximumLayoutSize(Container target) { + return computeDimension(target, 2); + } + + @Override + public void removeLayoutComponent(Component comp) { + this.components.remove(comp); + } +} diff --git a/editor.fold/src/org/netbeans/spi/editor/fold/ContentReader.java b/editor.fold/src/org/netbeans/spi/editor/fold/ContentReader.java new file mode 100644 --- /dev/null +++ b/editor.fold/src/org/netbeans/spi/editor/fold/ContentReader.java @@ -0,0 +1,97 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2013 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + * + * Contributor(s): + * + * Portions Copyrighted 2013 Sun Microsystems, Inc. + */ +package org.netbeans.spi.editor.fold; + +import javax.swing.text.BadLocationException; +import javax.swing.text.Document; +import org.netbeans.api.editor.fold.Fold; +import org.netbeans.api.editor.fold.FoldTemplate; +import org.netbeans.api.editor.fold.FoldType; +import org.netbeans.spi.editor.mimelookup.MimeLocation; + +/** + * Callback interface, which is called to extract description for a fold. + * The Reader will be called to produce a preview text to be displayed in the + * folded view. The reader will be called under document lock, although the fold + * hierarchy will not be locked. You may query the fold properties, but may not + * traverse the fold hierarchy. + *

+ * An instance of ContentReader is used repeatedly, i.e. if the same FoldTemplate + * is assigned to multiple folds. It is advised that the implementation of ContentReader + * is stateless. + * + * @author sdedic + */ +public interface ContentReader { + /** + * Acquires text for fold content. + * The method is executed under read lock on the Document. However, the Fold Hierarchy + * is not locked. Accessing fold offsets should be safe, but relationships with other Folds + * (parents, children, root) are not guarded. + *

+ * If the ContentReader cannot extract the contents (i.e. it does not want to handle the fold), + * {@code null} may be returned. If more ContentReaders are registered, some other instance might + * handle the fold properly. If not, the placeholder will be retained and presented in the fold + * text. + * + * @param d the document to read from + * @param f fold, whose contents should be read + * @param ft the original fold template, if available + * @return content to be displayed in the folded view, or {@code null} if unwilling to retrieve the content. + */ + public CharSequence read(Document d, Fold f, FoldTemplate ft) throws BadLocationException; + + /** + * Factory, which produces ContentReader instance(s) appropriate for the fold type. + * The returned instance may be used to read contents for all folds of the given type, in + * different documents (of the same mime type). + */ + @MimeLocation(subfolderName = "FoldManager") + public interface Factory { + /** + * @param ft the fold type + * @return ContentReader instance or {@code null}, if content should not be presented in the fold preview. + */ + public ContentReader createReader(FoldType ft); + } +} + diff --git a/editor.fold/src/org/netbeans/spi/editor/fold/FoldInfo.java b/editor.fold/src/org/netbeans/spi/editor/fold/FoldInfo.java new file mode 100644 --- /dev/null +++ b/editor.fold/src/org/netbeans/spi/editor/fold/FoldInfo.java @@ -0,0 +1,261 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2013 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + * + * Contributor(s): + * + * Portions Copyrighted 2013 Sun Microsystems, Inc. + */ +package org.netbeans.spi.editor.fold; + +import org.netbeans.api.editor.fold.FoldTemplate; +import org.netbeans.api.editor.fold.FoldType; +import org.openide.util.Parameters; + +/** + * The FoldInfo encapsulates the information passed to the + * {@link FoldOperation#addToHierarchy(org.netbeans.api.editor.fold.FoldType, int, int, org.netbeans.api.editor.fold.FoldTemplate, boolean, java.lang.Object, org.netbeans.spi.editor.fold.FoldHierarchyTransaction). + * Set of FoldInfos can be then applied to the FoldHieararchy, creating new, and removing obsolete Folds, so that + * Folds which prevail remain in the hierarchy. The mandatory information is start and end of the fold, and the {@link FoldType}. + * If necessary, a {@link FoldTemplate} attached to the FoldType can be overriden for the specific Fold instance. Note though, that + * if the FoldTemplate instance changes with the next fold update, the Fold may fire change events. + *

+ * Ultimately, it is possible to hand-override the Fold's description from the FoldTemplate provided value. + *

+ * If the FoldInfo is used to update an existing Fold, the or the FoldTemplate's properties + * collapsed state are updated to the existing Fold instance. Appropriate fold change event is fired. It is not possible + * to change the type of the fold. + *

+ * Initial folding state can be specified, for the case the fold will be created (it does not exist). If unspecified, + * the infrastructure can assign an appropriate state based on e.g. user preferences. Collapsed state is never changed + * for existing folds, even though FoldInfo specifies a value. + * + * Use {@link FoldUtilities#update} to perform the process. + * + * @author sdedic + */ +public final class FoldInfo { + /** + * Start of the folded region + */ + private int start; + + /** + * End of the folded region + */ + private int end; + + /** + * Tooltip contents and guarded areas of the fold. Defaults to the FoldTemplate present in the FoldType. + */ + private FoldTemplate template; + + /** + * The fold type of the fold + */ + private FoldType type; + + /** + * Determines whether the fold should be initially collapsed. Value of null means + * the collapsed state should be computed by the infrastructure. Non-null value will + * force the fold to expand or collapse. + */ + private Boolean collapsed; + + /** + * Extra information attached to a Fold + */ + private Object extraInfo; + + /** + * Custom description, overriding the default one in the template + */ + private String description; + + /** + * Creates a FoldInfo for the specified range. + * + * @param start start offset + * @param end end offset + * @param type type of the fold + * @return FoldInfo instance + */ + public static FoldInfo range(int start, int end, FoldType type) { + return new FoldInfo(start, end, type); + } + + private FoldInfo(int start, int end, FoldType ft) { + Parameters.notNull("ft", ft); + if (start < 0) { + throw new IllegalArgumentException("Invalid start offet: " + start); + } + if (end < start) { + throw new IllegalArgumentException("Invalid end offset: " + end + ", start is: " + start); + } + this.type = ft; + this.start = start; + this.end = end; + this.template = ft.getTemplate(); + } + + /** + * Attaches FoldTemplate to the FoldInfo. + * The instance will be used to configure or update the Fold instance in preference to {@link FoldType#getTemplate}. + * + * @param t fold template + * @return this instance + */ + public FoldInfo withTemplate(FoldTemplate t) { + Parameters.notNull("t", t); + this.template = t; + return this; + } + + /** + * Use to provide a custom description for the fold. + * The description overrides all other ones taken from FoldTemplates. The description can use + * content placeholder (see {@link FoldTemplate} for explanation. When {@code null} is set, the + * description Fold reverts back to the one provided by FoldTemplates (the override is cleared). + * + * @param desc description text. + * @return this instance + */ + public FoldInfo withDescription(String desc) { + this.description = desc; + return this; + } + + /** + * Attaches custom extra info to the fold. + * The extra info will be available from {@link org.netbeans.api.editor.fold.Fold#getExtraInfo. + * + * @param extraInfo custom data + * @return this instance + */ + public FoldInfo attach(Object extraInfo) { + this.extraInfo = extraInfo; + return this; + } + + /** + * Returns description override. + * When {@code null}, information from FoldTemplates should be used. + * + * @return explicit description, or {@code null}. + */ + public String getDescriptionOverride() { + return description; + } + + /** + * Returns the extra information attached to a fold. + * + * @return data, or {@code null} if no data is present + */ + public Object getExtraInfo() { + return extraInfo; + } + + /** + * Records the desired collapsed state. + * + * @param state the desired collapsed state + * @return this instance. + */ + public FoldInfo collapsed(boolean state) { + this.collapsed = state; + return this; + } + + /** + * Provides start offset of the folded content + * @return offset into the document + */ + public int getStart() { + return start; + } + + /** + * Provides end offset of the folded content + * @return offset into the document + */ + public int getEnd() { + return end; + } + + /** + * Provides FoldTemplate to be used with the Fold. + * The template will be used in preference to {@link org.netbeans.api.editor.fold.FoldType#getTemplate}. + * {@code Null} return value means that the FoldTemplate from the FoldType is in effect. + + * @return FoldTemplate instance, or {@code null} + */ + public FoldTemplate getTemplate() { + return template; + } + + /** + * Provides FoldType for the fold. + * The FoldType will be assigned to the new fold. If type of a fold (occupying the same range) changes during + * {@link FoldOperation#update}, the fold will be destroyed and re-created. It is not possible to change FoldType + * of a Fold. + * + * @return FoldType instance, never {@code null}. + */ + public FoldType getType() { + return type; + } + + /** + * Provides the desired collapsed state or {@code null}, if no specific + * state is required. + * When {@code null} is reported, the infrastructure can assign an an appropriate initial state to the fold, + * e.g. based on user preferences. States of existing fold is never changed during update. + * + * @return desired collapsed state or {@code null} + */ + public Boolean getCollapsed() { + return collapsed; + } + + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("FoldInfo[").append(start).append(" - ").append(end). + append(", ").append(type).append(", desc = ").append(description == null ? template.getDescription() : description). + append(" collapsed = ").append(collapsed).append("]"); + return sb.toString(); + } +} diff --git a/editor.fold/src/org/netbeans/spi/editor/fold/FoldOperation.java b/editor.fold/src/org/netbeans/spi/editor/fold/FoldOperation.java --- a/editor.fold/src/org/netbeans/spi/editor/fold/FoldOperation.java +++ b/editor.fold/src/org/netbeans/spi/editor/fold/FoldOperation.java @@ -44,14 +44,19 @@ package org.netbeans.spi.editor.fold; +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; import javax.swing.text.BadLocationException; import org.netbeans.api.editor.fold.Fold; import org.netbeans.api.editor.fold.FoldHierarchy; +import org.netbeans.api.editor.fold.FoldTemplate; import org.netbeans.api.editor.fold.FoldType; import org.netbeans.modules.editor.fold.ApiPackageAccessor; import org.netbeans.modules.editor.fold.FoldHierarchyTransactionImpl; import org.netbeans.modules.editor.fold.FoldOperationImpl; import org.netbeans.modules.editor.fold.SpiPackageAccessor; +import org.openide.util.Parameters; /** @@ -128,6 +133,8 @@ * {@link #getExtraInfo(org.netbeans.api.editor.fold.Fold)}. * * @return new fold instance that was added to the hierarchy. + * @deprecated please use {@link #addToHierarchy(org.netbeans.api.editor.fold.FoldType, int, int, java.lang.Boolean, org.netbeans.api.editor.fold.FoldTemplate, java.lang.String, java.lang.Object, org.netbeans.spi.editor.fold.FoldHierarchyTransaction)}. + * This form of call does not support automatic state assignment and fold templates. */ public Fold addToHierarchy(FoldType type, String description, boolean collapsed, int startOffset, int endOffset, int startGuardedLength, int endGuardedLength, @@ -142,6 +149,66 @@ } /** + * Adds a fold to the hierarchy. + * The description and the guarded start/end is taken from the 'template' FoldTemplate. As the fold template display + * is the most common override, the override string can be passed in 'displayOverride' (and will be used instead + * of template and instead of type's template). + *

+ * The collapsed state can be prescribed, but can {@code null} can be passed to indicate the infrastructure should + * assign collapsed state based on e.g. user preferences. The exact assignment algorithm is left unspecified. Callers + * are recommended not to assign collapsed/expanded state explicitly. + *

+ * Usually, it's OK to pass null for collapsed, template and possibly extraInfo. + *

+ * Events produced by this add- call will be fired when the 'transaction' is committed. However fold hierarch will + * be changed immediately. + * + * @param type type of the fold, cannot be {@code null} + * @param startOffset starting offset + * @param endOffset end offset + * @param collapsed the initial collapsed state; if {@code null}, the state will be assigned automatically. + * @param template the FoldTemplate to use instead of default template of the type. {@code null}, if the type's template should be used. + * @param extraInfo arbitrary extra information specific for the fold being created. + * It's not touched or used by the folding infrastructure in any way. + * null can be passed if there is no extra information. + *
+ * The extra info of the existing fold can be obtained by + * {@link #getExtraInfo(org.netbeans.api.editor.fold.Fold)}. + * @param transaction the transaction that manages events, cannot be null. + * @return the created Fold instance + * @throws BadLocationException + * @since 1.34 + */ + public Fold addToHierarchy( + FoldType type, + int startOffset, int endOffset, + Boolean collapsed, + FoldTemplate template, String displayOverride, + Object extraInfo, FoldHierarchyTransaction transaction) + throws BadLocationException { + Parameters.notNull("type", type); + Parameters.notNull("transaction", transaction); + + boolean c; + if (collapsed == null) { + c = impl.getInitialState(type); + } else { + c = collapsed; + } + if (template == null) { + template = type.getTemplate(); + } + if (displayOverride == null) { + displayOverride = template.getDescription(); + } + Fold fold = impl.createFold(type, displayOverride, + c, startOffset, endOffset, template.getGuardedStart(), + template.getGuardedEnd(), extraInfo); + impl.addToHierarchy(fold, transaction.getImpl()); + return fold; + } + + /** * This static method can be used to check whether the bounds * of the fold that is planned to be added are valid. *
@@ -270,22 +337,89 @@ return impl.getHierarchy(); } + /** + * Informs that the manager was released. + * Use the method to check whether the {@link FoldManager} should be still operational. + * Once released, the FoldManager (and its Operation) will not be used again by the infrastructure. + * + * @return true, if release() was called on the manager + * @since 1.34 + */ public boolean isReleased() { return impl.isReleased(); } + /** + * Enumerates all Folds defined by this Operation, in the document-range order. + * Outer folds precede the inner ones. Folds, which overlap are enumerated strictly + * in the order of their starting positions. + *

+ * The method may be only called under {@link FoldHierarchy#lock}. The Iterator may + * be only used until that lock is released. After releasing the lock, the Iterator + * may fail. + * + * @return readonly iterator for all folds defined through this FoldOperation + * @since 1.34 + */ + public Iterator foldIterator() { + return impl.foldIterator(); + } + + /** + * Performs refresh of folding information. The method will: + *

    + *
  • remove Folds, which do not appear among the supplied FoldInfos + *
  • add Folds, which do not exist, but are described by some FoldInfo + *
  • attempt to update Folds, which match the FoldInfos + *
+ * For each of the supplied FoldInfos, there should be at most 1 Fold either created or found existing, and no + * Folds without a corresponding input FoldInfo should remain in the hierarchy after the call. The mapping from the + * input FoldInfo to the corresponding Fold (created or found existing) is returned. + *

+ * Note that Folds, which are blocked (e.g. by a higher-priority manager) will be added/removed/updated and + * returned as well. In order to find whether a specific Fold is blocked, please call {@link #isBlocked}. + *

+ * If the {@code removed} or {@code created} parameters are not null, the removed Fold instances, or the FoldInfos + * that created new Folds will be put into those collection as a supplemental return value. The caller may then + * update its own data with respect to the changes and the current Fold set. + *

+ * Note: The method may be only called under {@link FoldHierarchy#lock}. This implies the document is also read-locked. + * The caller must check whether a modification happen in between the FoldInfos were produced at the + * document + hierarchy lock. The method creates and commits its own {@link FoldTransaction} - they are not reentrant, + * so do not call the method under a transaction. + * + * @param infos current state of folds that should be updated to the hierarchy + * @param removed Collection that will receive Folds, which have been removed from the hierarchy, or {@code null}, if the caller + * does not want to receive the information + * @param created Collection that will receive FoldInfos that created new Folds in the hierarchy, {@code null} means + * the caller is not interested in the creation information. + * + * @return the mapping from FoldInfos supplied as an input to current Folds. {@code null}, if the manager has been + * released. + * @since 1.34 + */ + public Map update( + Collection infos, + Collection removed, + Collection created) throws BadLocationException { + Parameters.notNull("infos", infos); + return impl.update(infos, removed, created); + } private static final class SpiPackageAccessorImpl extends SpiPackageAccessor { + @Override public FoldHierarchyTransaction createFoldHierarchyTransaction( FoldHierarchyTransactionImpl impl) { return new FoldHierarchyTransaction(impl); } + @Override public FoldHierarchyTransactionImpl getImpl(FoldHierarchyTransaction transaction) { return transaction.getImpl(); } + @Override public FoldOperation createFoldOperation(FoldOperationImpl impl) { return new FoldOperation(impl); } diff --git a/editor.fold/src/org/netbeans/spi/editor/fold/FoldTypeProvider.java b/editor.fold/src/org/netbeans/spi/editor/fold/FoldTypeProvider.java new file mode 100644 --- /dev/null +++ b/editor.fold/src/org/netbeans/spi/editor/fold/FoldTypeProvider.java @@ -0,0 +1,85 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2012 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + * + * Contributor(s): + * + * Portions Copyrighted 2012 Sun Microsystems, Inc. + */ +package org.netbeans.spi.editor.fold; + +import java.util.Collection; +import org.netbeans.api.editor.fold.FoldType; +import org.netbeans.spi.editor.mimelookup.MimeLocation; + +/** + * Provider of FoldType constants for the MimeType. + * The Provider should enumerate FoldTypes that apply to the given MIME type. + * There can be multiple providers for a MIME type - some advanced constructions in + * the language can be recognized / folded by extension modules. Consider Java vs. + * Bean Patterns, or XML vs. Spring bean config. + *

+ * FoldTypes will be collected and some pieces of UI can present the folds, such + * as Auto-folding options. + *

+ * The Provider may specify inheritable=true; in that case the contributed FoldTypes + * will become available for more specific MIME types, too. For example, if a FoldTypeProvider + * for text/xml registers FoldTypes TAG and COMMENT with inheritable=true, + * those FoldTypes will be listed also for text/x-ant+xml. This feature allows + * to "inject" Fold types and FoldManager on general MIME type ("") for all + * types of files. + * + * @author sdedic + * @since 1.34 + */ +@MimeLocation(subfolderName = "FoldManager") +public interface FoldTypeProvider { + /** + * Enumerates values for the given type. + * @return FoldType values. + */ + public Collection getValues(Class type); + + /** + * Determines whether the folds propagate to child mime types(paths). + * If the method returns true, then more specific MIME types will also + * list FoldTypes returned by this Provider. + * + * @return whether the provided FoldTypes should be inherited (true). + */ + public boolean inheritable(); + +} diff --git a/editor.fold/test/unit/src/org/netbeans/modules/editor/fold/FoldHierarchyTestEnv.java b/editor.fold/test/unit/src/org/netbeans/modules/editor/fold/FoldHierarchyTestEnv.java --- a/editor.fold/test/unit/src/org/netbeans/modules/editor/fold/FoldHierarchyTestEnv.java +++ b/editor.fold/test/unit/src/org/netbeans/modules/editor/fold/FoldHierarchyTestEnv.java @@ -68,7 +68,7 @@ this(new FoldManagerFactory[] { factory }); } - FoldHierarchyTestEnv(FoldManagerFactory[] factories) { + FoldHierarchyTestEnv(FoldManagerFactory... factories) { pane = new JEditorPane(); assert (getMimeType() != null); diff --git a/editor.fold/test/unit/src/org/netbeans/modules/editor/fold/FoldOperationTest.java b/editor.fold/test/unit/src/org/netbeans/modules/editor/fold/FoldOperationTest.java new file mode 100644 --- /dev/null +++ b/editor.fold/test/unit/src/org/netbeans/modules/editor/fold/FoldOperationTest.java @@ -0,0 +1,559 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2013 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + * + * Contributor(s): + * + * Portions Copyrighted 2013 Sun Microsystems, Inc. + */ +package org.netbeans.modules.editor.fold; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import javax.swing.JPanel; +import javax.swing.SwingUtilities; +import javax.swing.event.DocumentEvent; +import javax.swing.text.AbstractDocument; +import javax.swing.text.BadLocationException; +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertNotNull; +import static junit.framework.Assert.assertSame; +import static junit.framework.Assert.assertTrue; +import org.netbeans.api.editor.fold.Fold; +import org.netbeans.api.editor.fold.FoldHierarchy; +import org.netbeans.api.editor.fold.FoldHierarchyEvent; +import org.netbeans.api.editor.fold.FoldHierarchyListener; +import org.netbeans.api.editor.fold.FoldStateChange; +import org.netbeans.api.editor.fold.FoldTemplate; +import org.netbeans.api.editor.fold.FoldType; +import org.netbeans.api.editor.fold.FoldUtilities; +import org.netbeans.junit.NbTestCase; +import org.netbeans.spi.editor.fold.FoldHierarchyTransaction; +import org.netbeans.spi.editor.fold.FoldInfo; +import org.netbeans.spi.editor.fold.FoldManager; +import org.netbeans.spi.editor.fold.FoldManagerFactory; +import org.netbeans.spi.editor.fold.FoldOperation; + +/** + * + * @author sdedic + */ +public class FoldOperationTest extends NbTestCase { + + public FoldOperationTest(String name) { + super(name); + } + + /* + * update* tests use a simple fold manager, which defines several folds, some of them nested. + * A[0,1] + * B[2,7] + * C[3,4] + * D[5,6] + * E[8,9] + */ + + private static int[][] foldRanges = new int[][] { + {0, 1}, + {3,12}, + {5, 6}, + {9, 10}, + {14, 16} + }; + + /** + * Checks that no changes (update with the same infos) will not fire events, remove or add any folds. + * + * @throws Exception + */ + public void testUpdateNoChanges() throws Exception { + final TestFoldManager[] mgr = new TestFoldManager[1]; + FoldHierarchyTestEnv env = new FoldHierarchyTestEnv(new FoldManagerFactory() { + @Override + public FoldManager createFoldManager() { + return mgr[0] = new TestFoldManager(); + } + }); + AbstractDocument doc = env.getDocument(); + doc.insertString(0, "12345678901234567890", null); + + FoldHierarchy hierarchy = env.getHierarchy(); + + Collection infos = new ArrayList(10); + infos.add(FoldInfo.range(0, 1, FoldType.MEMBER)); + infos.add(FoldInfo.range(9, 10, FoldType.MEMBER)); + infos.add(FoldInfo.range(14, 16, FoldType.MEMBER)); + // not sorted, check :) + infos.add(FoldInfo.range(3, 12, FoldType.MEMBER)); + infos.add(FoldInfo.range(5, 6, FoldType.MEMBER)); + + doc.readLock(); + try { + hierarchy.lock(); + TestFoldManager m = mgr[0]; + try { + Collection remove = new ArrayList(); + Collection create = new ArrayList(); + + final boolean[] changed = new boolean[1]; + + hierarchy.addFoldHierarchyListener(new FoldHierarchyListener() { + + @Override + public void foldHierarchyChanged(FoldHierarchyEvent evt) { + changed[0] = true; + } + }); + + Map mapping = m.operation.update(infos, remove, create); + + assertEquals(m.initFolds.size(), mapping.size()); + assertTrue(m.initFolds.containsAll(mapping.values())); + assertFalse(changed[0]); + + } finally { + hierarchy.unlock(); + } + } finally { + doc.readUnlock(); + } + } + + private List createDefaultInfos() { + List infos = new ArrayList(10); + infos.add(FoldInfo.range(0, 1, FoldType.MEMBER)); + infos.add(FoldInfo.range(3, 12, FoldType.MEMBER)); + infos.add(FoldInfo.range(5, 6, FoldType.MEMBER)); + infos.add(FoldInfo.range(9, 10, FoldType.MEMBER)); + infos.add(FoldInfo.range(14, 16, FoldType.MEMBER)); + return infos; + } + + /** + * Checks that folds are created beween two folds, encapsulating existing folds. + * + * @throws Exception + */ + public void testUpdateCreateFold() throws Exception { + final TestFoldManager[] mgr = new TestFoldManager[1]; + FoldHierarchyTestEnv env = new FoldHierarchyTestEnv(new FoldManagerFactory() { + @Override + public FoldManager createFoldManager() { + return mgr[0] = new TestFoldManager(); + } + }); + AbstractDocument doc = env.getDocument(); + doc.insertString(0, "12345678901234567890", null); + + FoldHierarchy hierarchy = env.getHierarchy(); + + List infos = createDefaultInfos(); + + // add a new fold between #1 and #2 + infos.add(FoldInfo.range(2, 3, FoldType.MEMBER)); + + // add a new fold at the end: + infos.add(FoldInfo.range(19,20, FoldType.MEMBER)); + + // add a fold, which encapsulates #2 - #5 + infos.add(FoldInfo.range(3, 18, FoldType.MEMBER)); + + // add a fold, which encapsulates ##5 + infos.add(FoldInfo.range(13, 16, FoldType.MEMBER)); + + doc.readLock(); + try { + hierarchy.lock(); + TestFoldManager m = mgr[0]; + try { + Collection remove = new ArrayList(); + Collection create = new ArrayList(); + + final boolean[] changed = new boolean[1]; + + hierarchy.addFoldHierarchyListener(new FoldHierarchyListener() { + @Override + public void foldHierarchyChanged(FoldHierarchyEvent evt) { + changed[0] = true; + } + }); + Map mapping = m.operation.update(infos, remove, create); + + // 3 folds added, no deleted: + assertEquals(4, create.size()); + assertEquals(0, remove.size()); + + } finally { + hierarchy.unlock(); + } + } finally { + doc.readUnlock(); + } + } + + /** + * Checks that updated folds can SHIFT. If a fold should be replaced by a same-type fold, which + * fully contains the original one, or is fully contained by the original one, the original fold will + * be updated rather than removed/created. This behaviour eliminates issues with collapsing imports. + * + * @throws Exception + */ + public void testUpdateShiftFolds() throws Exception { + final TestFoldManager[] mgr = new TestFoldManager[1]; + FoldHierarchyTestEnv env = new FoldHierarchyTestEnv(new FoldManagerFactory() { + @Override + public FoldManager createFoldManager() { + return mgr[0] = new TestFoldManager(); + } + }); + AbstractDocument doc = env.getDocument(); + doc.insertString(0, "12345678901234567890", null); + + FoldHierarchy hierarchy = env.getHierarchy(); + + List infos = createDefaultInfos(); + + // Fold #3 is not shifted, as the new fold does not intersect with the old one. Fold will be replaced. + FoldInfo cInfo; + infos.add(cInfo = FoldInfo.range(4, 5, FoldType.MEMBER)); + + // Fold #4 is extended backwards + infos.add(FoldInfo.range(8, 10, FoldType.MEMBER)); + + // Fold #5 is extended in both directions + infos.add(FoldInfo.range(13, 17, FoldType.MEMBER)); + + infos.remove(4); infos.remove(3); infos.remove(2); + + TestFoldManager m = mgr[0]; + final Fold fold8 = FoldUtilities.findNearestFold(hierarchy, 8); + final Fold fold13 = FoldUtilities.findNearestFold(hierarchy, 13); + + class FHL implements FoldHierarchyListener { + boolean changed; + + @Override + public void foldHierarchyChanged(FoldHierarchyEvent evt) { + changed = true; + assertEquals(2, evt.getFoldStateChangeCount()); + for (int i = evt.getFoldStateChangeCount() - 1; i >= 0; i--) { + FoldStateChange chg = evt.getFoldStateChange(i); + if (chg.getFold() == fold8) { + assertEquals(9, chg.getOriginalStartOffset()); + assertEquals(-1, chg.getOriginalEndOffset()); + } else if (chg.getFold() == fold13) { + assertEquals(14, chg.getOriginalStartOffset()); + assertEquals(16, chg.getOriginalEndOffset()); + } else { + fail("Unexpected change"); + } + } + } + } + + FHL fhl = new FHL(); + doc.readLock(); + try { + hierarchy.lock(); + try { + Collection remove = new ArrayList(); + Collection create = new ArrayList(); + + hierarchy.addFoldHierarchyListener(fhl); + Map mapping = m.operation.update(infos, remove, create); + + // 3 folds added, no deleted: + assertEquals(1, create.size()); + assertEquals(1, remove.size()); + + assertSame(cInfo, create.iterator().next()); + + Fold f = remove.iterator().next(); + // old fold + assertEquals(5, f.getStartOffset()); + + // new fold + FoldInfo newInfo = create.iterator().next(); + assertSame(cInfo, newInfo); + f = mapping.get(cInfo); + assertEquals(4, f.getStartOffset()); + } finally { + hierarchy.unlock(); + } + } finally { + doc.readUnlock(); + } + } + + /** + * Checks that a released operation will not update anything + * + * @throws Exception + */ + public void testNoUpdateAfterRelease() throws Exception { + final TestFoldManager[] mgr = new TestFoldManager[1]; + final FoldHierarchyTestEnv env = new FoldHierarchyTestEnv(new FoldManagerFactory() { + @Override + public FoldManager createFoldManager() { + return mgr[0] = new TestFoldManager(); + } + }); + AbstractDocument doc = env.getDocument(); + doc.insertString(0, "12345678901234567890", null); + + FoldHierarchy hierarchy = env.getHierarchy(); + List infos = createDefaultInfos(); + + // add a new fold between #1 and #2 + infos.add(FoldInfo.range(2, 3, FoldType.MEMBER)); + + // add a new fold at the end: + infos.add(FoldInfo.range(19,20, FoldType.MEMBER)); + + // add a fold, which encapsulates #2 - #5 + infos.add(FoldInfo.range(3, 18, FoldType.MEMBER)); + + infos.remove(4); infos.remove(3); infos.remove(2); + + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + JPanel outer = new JPanel(); + // force parent change + outer.add(env.getPane()); + } + }); + // listener is attached in between these events + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + // nothing, just wait for the delayed events to process. + } + }); + FoldHierarchyExecution.waitAllTasks(); + + doc.readLock(); + try { + hierarchy.lock(); + TestFoldManager m = mgr[0]; + try { + + Collection remove = new ArrayList(); + Collection create = new ArrayList(); + final boolean[] changed = new boolean[1]; + + hierarchy.addFoldHierarchyListener(new FoldHierarchyListener() { + @Override + public void foldHierarchyChanged(FoldHierarchyEvent evt) { + changed[0] = true; + } + }); + Map mapping = m.operation.update(infos, remove, create); + assertNull(mapping); + // 3 folds added, no deleted: + assertEquals(0, create.size()); + assertEquals(0, remove.size()); + assertFalse(changed[0]); + + } finally { + hierarchy.unlock(); + } + } finally { + doc.readUnlock(); + } + } + + + /** + * Checks that foldIterator() enumerates blocked folds, and blocked folds blocked by blocked folds + * + * @throws Exception + */ + public void testFoldIterator() throws Exception { + TestFoldManager baseFoldManager = new TestFoldManager(BASE_RANGE, FoldType.MEMBER); + TestFoldManager overrideFoldManager = new TestFoldManager(OVERRIDE_RANGE, FoldType.NESTED); + TestFoldManager uberFoldManager = new TestFoldManager(UBER_RANGE, FoldType.USER); + + class FMF implements FoldManagerFactory { + private FoldManager fm; + + public FMF(FoldManager fm) { + this.fm = fm; + } + + @Override + public FoldManager createFoldManager() { + return fm; + } + } + + final FoldHierarchyTestEnv env = new FoldHierarchyTestEnv( + new FMF(uberFoldManager), new FMF(overrideFoldManager), new FMF(baseFoldManager) + ); + AbstractDocument doc = env.getDocument(); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 20; i++) { + sb.append("12345678901234567890"); + } + doc.insertString(0, sb.toString(), null); + + FoldHierarchy hierarchy = env.getHierarchy(); + hierarchy.lock(); + // now update the uber-range, so that the new fold blocks the blocking fold: + FoldOperation fo = uberFoldManager.operation; + FoldHierarchyTransaction t = fo.openTransaction(); + fo.addToHierarchy(FoldType.USER, 110, 170, null, null, null, null, t); + t.commit(); + + // check that the hierarchy contains the proper overrides: + Fold f = FoldUtilities.findOffsetFold(hierarchy, 2); + assertNotNull(f); + assertSame(FoldType.NESTED, f.getType()); + + f = FoldUtilities.findOffsetFold(hierarchy, 122); + assertNotNull(f); + assertSame(FoldType.USER, f.getType()); + + // enumerate all folds of the base iterator + fo = baseFoldManager.operation; + + Iterator folds = fo.foldIterator(); + for (int i = 0; i < BASE_RANGE.length; i++) { + int[] rng = BASE_RANGE[i]; + f = folds.next(); + + assertSame(FoldType.MEMBER, f.getType()); + assertEquals(rng[0], f.getStartOffset()); + assertEquals(rng[1], f.getEndOffset()); + } + assertFalse(folds.hasNext()); + } + + private static int[][] BASE_RANGE = { + { 2, 4 }, + { 7, 100}, + { 10, 30 }, + { 40, 50 }, + { 120, 200 }, + { 130, 140 }, + { 150, 160 } + }; + + private static int[][] OVERRIDE_RANGE = { + { 1, 3 }, + { 7, 100}, + { 8, 20 }, // intersect at the beginning + { 45, 60 }, // intersect at the end + { 120, 200 }, // block + { 125, 142 }, // fully contain + { 155, 157 } // proper child + }; + + private static int[][] UBER_RANGE = { + }; + + private class TestFoldManager implements FoldManager { + FoldOperation operation; + Collection initFolds = new ArrayList(6); + int[][] ranges; + FoldType type; + + public TestFoldManager() { + ranges = foldRanges; + type = FoldType.MEMBER; + } + + public TestFoldManager(int[][] ranges, FoldType type) { + this.ranges = ranges; + this.type = type; + } + + + @Override + public void init(FoldOperation operation) { + this.operation = operation; + } + + public void initFolds(FoldHierarchyTransaction transaction) { + for (int i = 0; i < ranges.length; i++) { + int[] range = ranges[i]; + try { + initFolds.add(operation.addToHierarchy( + type, + range[0], + range[1], + null, + null, + null, + null, transaction + )); + } catch (BadLocationException ex) { + } + } + } + + @Override + public void insertUpdate(DocumentEvent evt, FoldHierarchyTransaction transaction) { + } + + @Override + public void removeUpdate(DocumentEvent evt, FoldHierarchyTransaction transaction) { + } + + @Override + public void changedUpdate(DocumentEvent evt, FoldHierarchyTransaction transaction) { + } + + @Override + public void removeEmptyNotify(Fold epmtyFold) { + } + + @Override + public void removeDamagedNotify(Fold damagedFold) { + } + + @Override + public void expandNotify(Fold expandedFold) { + } + + @Override + public void release() { + // purposely ignore + } + } +} diff --git a/editor.lib/apichanges.xml b/editor.lib/apichanges.xml --- a/editor.lib/apichanges.xml +++ b/editor.lib/apichanges.xml @@ -107,6 +107,36 @@ + +

Removed dependencies on Folding + + + + + +

+ Inheritance of FoldHierarchyListener was removed, as it was the last dependency on + fold API from the editor.lib module. +

+
+ + + + + Allow to refresh editor view so that caret is visible + + + + + +

+ The method helps to ensure that the caret, if it was originally visible on the screen, + will remain visible after some view hierarchy change (i.e. define a new fold). +

+
+ + +
Moving find implementations to module editor.search diff --git a/editor.lib/arch.xml b/editor.lib/arch.xml --- a/editor.lib/arch.xml +++ b/editor.lib/arch.xml @@ -632,6 +632,15 @@ To specify modifiers for which the hyperlinking should be enabled, or to switch the hyperlinking off. Valid values are "[CSMA]+" (to specify combination of modifiers) or "off" (to switch hyperlinking off). + + The client property must be defined on JTextComponent managed by the NetBeans editor. +

+ Mouse gestures require to determine whether the point at caret is folded or not. Plain text is then selected. + The client property org.netbeans.api.fold.expander (if defined) should contains a Callable<Boolean> that + returns false, if the point is a plaintext, true otherwise. Fold expansion should be handled by the Callable. +

+ editor.fold module uses this client property to hook into BaseCaret processing. + diff --git a/editor.lib/module-auto-deps.xml b/editor.lib/module-auto-deps.xml --- a/editor.lib/module-auto-deps.xml +++ b/editor.lib/module-auto-deps.xml @@ -62,5 +62,15 @@ + + + + + + + + + + diff --git a/editor.lib/nbproject/project.properties b/editor.lib/nbproject/project.properties --- a/editor.lib/nbproject/project.properties +++ b/editor.lib/nbproject/project.properties @@ -42,7 +42,7 @@ javac.compilerargs=-Xlint:unchecked javac.source=1.6 -spec.version.base=3.36.0 +spec.version.base=3.37.0 is.autoload=true javadoc.arch=${basedir}/arch.xml diff --git a/editor.lib/nbproject/project.xml b/editor.lib/nbproject/project.xml --- a/editor.lib/nbproject/project.xml +++ b/editor.lib/nbproject/project.xml @@ -59,14 +59,6 @@ - org.netbeans.modules.editor.fold - - - - 1 - - - org.netbeans.modules.editor.indent diff --git a/editor.lib/src/org/netbeans/editor/ActionFactory.java b/editor.lib/src/org/netbeans/editor/ActionFactory.java --- a/editor.lib/src/org/netbeans/editor/ActionFactory.java +++ b/editor.lib/src/org/netbeans/editor/ActionFactory.java @@ -84,10 +84,6 @@ import org.netbeans.api.editor.EditorActionNames; import org.netbeans.api.editor.EditorActionRegistration; import org.netbeans.api.editor.EditorActionRegistrations; -import org.netbeans.api.editor.fold.Fold; -import org.netbeans.api.editor.fold.FoldHierarchy; -import org.netbeans.api.editor.fold.FoldUtilities; -import org.netbeans.api.lexer.TokenHierarchy; import org.netbeans.api.progress.ProgressUtils; import org.netbeans.lib.editor.util.swing.DocumentUtilities; import org.netbeans.lib.editor.util.swing.PositionRegion; @@ -2106,192 +2102,6 @@ } - /** Returns the fold that should be collapsed/expanded in the caret row - * @param hierarchy hierarchy under which all folds should be collapsed/expanded. - * @param dot caret position offset - * @param lineStart offset of the start of line - * @param lineEnd offset of the end of line - * @return the fold that meet common criteria in accordance with the caret position - */ - private static Fold getLineFold(FoldHierarchy hierarchy, int dot, int lineStart, int lineEnd){ - Fold caretOffsetFold = FoldUtilities.findOffsetFold(hierarchy, dot); - - // beginning searching from the lineStart - Fold fold = FoldUtilities.findNearestFold(hierarchy, lineStart); - - while (fold!=null && - (fold.getEndOffset()<=dot || // find next available fold if the 'fold' is one-line - // or it has children and the caret is in the fold body - // i.e. class A{ |public void method foo(){}} - (!fold.isCollapsed() && fold.getFoldCount() > 0 && fold.getStartOffset()+10) ? fold.getStartOffset()+1 : fold.getEndOffset()); - if (nextFold!=null && nextFold.getStartOffset()lineEnd) { - - // in the case: - // class A{ - // } | - // try to find an offset fold on the offset of the line beginning - if (caretOffsetFold == null){ - caretOffsetFold = FoldUtilities.findOffsetFold(hierarchy, lineStart); - } - - return caretOffsetFold; - } - - // no fold at offset found, in this case return the fold - if (caretOffsetFold == null) return fold; - - // skip possible inner class members validating if the innerclass fold is collapsed - if (caretOffsetFold.isCollapsed()) return caretOffsetFold; - - // in the case: - // class A{ - // public vo|id foo(){} } - // 'fold' (in this case fold of the method foo) will be returned - if ( caretOffsetFold.getEndOffset()>fold.getEndOffset() && - fold.getEndOffset()>dot){ - return fold; - } - - // class A{ - // |} public void method foo(){} - // inner class fold will be returned - if (fold.getStartOffset()>caretOffsetFold.getEndOffset()) return caretOffsetFold; - - // class A{ - // public void foo(){} |} - // returning innerclass fold - if (fold.getEndOffset() dot || foldRowEnd < dot) return false; // it's not fold encapsulating dot - return true; - } - - - public void actionPerformed(ActionEvent evt, final JTextComponent target) { - Document doc = target.getDocument(); - doc.render(new Runnable() { - @Override - public void run() { - FoldHierarchy hierarchy = FoldHierarchy.get(target); - int dot = target.getCaret().getDot(); - hierarchy.lock(); - try{ - try{ - int rowStart = javax.swing.text.Utilities.getRowStart(target, dot); - int rowEnd = javax.swing.text.Utilities.getRowEnd(target, dot); - Fold fold = FoldUtilities.findNearestFold(hierarchy, rowStart); - fold = getLineFold(hierarchy, dot, rowStart, rowEnd); - if (fold==null){ - return; // no success - } - // ensure we' got the right fold - if (dotInFoldArea(target, fold, dot)){ - hierarchy.collapse(fold); - } - }catch(BadLocationException ble){ - ble.printStackTrace(); - } - }finally { - hierarchy.unlock(); - } - } - }); - } - } - - /** Expand a fold. Depends on the current caret position. */ - @EditorActionRegistration(name = BaseKit.expandFoldAction, - menuText = "#" + BaseKit.expandFoldAction + "_menu_text") - public static class ExpandFold extends LocalBaseAction { - public ExpandFold(){ - } - - public void actionPerformed(ActionEvent evt, final JTextComponent target) { - Document doc = target.getDocument(); - doc.render(new Runnable() { - @Override - public void run() { - FoldHierarchy hierarchy = FoldHierarchy.get(target); - int dot = target.getCaret().getDot(); - hierarchy.lock(); - try { - try { - int rowStart = javax.swing.text.Utilities.getRowStart(target, dot); - int rowEnd = javax.swing.text.Utilities.getRowEnd(target, dot); - Fold fold = getLineFold(hierarchy, dot, rowStart, rowEnd); - if (fold != null) { - hierarchy.expand(fold); - } - } catch (BadLocationException ble) { - ble.printStackTrace(); - } - } finally { - hierarchy.unlock(); - } - } - }); - } - } - - /** Collapse all existing folds in the document. */ - @EditorActionRegistration(name = BaseKit.collapseAllFoldsAction, - menuText = "#" + BaseKit.collapseAllFoldsAction + "_menu_text") - public static class CollapseAllFolds extends LocalBaseAction { - public CollapseAllFolds(){ - } - - public void actionPerformed(ActionEvent evt, JTextComponent target) { - FoldHierarchy hierarchy = FoldHierarchy.get(target); - // Hierarchy locking done in the utility method - FoldUtilities.collapseAll(hierarchy); - } - } - - /** Expand all existing folds in the document. */ - @EditorActionRegistration(name = BaseKit.expandAllFoldsAction, - menuText = "#" + BaseKit.expandAllFoldsAction + "_menu_text") - public static class ExpandAllFolds extends LocalBaseAction { - public ExpandAllFolds(){ - } - - public void actionPerformed(ActionEvent evt, JTextComponent target) { - FoldHierarchy hierarchy = FoldHierarchy.get(target); - // Hierarchy locking done in the utility method - FoldUtilities.expandAll(hierarchy); - } - } - /** Expand all existing folds in the document. */ @EditorActionRegistration(name = "dump-view-hierarchy") public static class DumpViewHierarchyAction extends LocalBaseAction { @@ -2303,15 +2113,19 @@ public void actionPerformed(ActionEvent evt, JTextComponent target) { AbstractDocument adoc = (AbstractDocument)target.getDocument(); + /* + * Folding has moved to editor.fold module. It must somehow hook + * into this dump to provide the information. + // Dump fold hierarchy FoldHierarchy hierarchy = FoldHierarchy.get(target); adoc.readLock(); try { hierarchy.lock(); try { - /*DEBUG*/System.err.println("FOLD HIERARCHY DUMP:\n" + hierarchy); // NOI18N + /*DEBUG* /System.err.println("FOLD HIERARCHY DUMP:\n" + hierarchy); // NOI18N TokenHierarchy th = TokenHierarchy.get(adoc); - /*DEBUG*/System.err.println("TOKEN HIERARCHY DUMP:\n" + (th != null ? th : "")); // NOI18N + /*DEBUG* /System.err.println("TOKEN HIERARCHY DUMP:\n" + (th != null ? th : "")); // NOI18N } finally { hierarchy.unlock(); @@ -2319,7 +2133,7 @@ } finally { adoc.readUnlock(); } - + */ View rootView = null; TextUI textUI = target.getUI(); if (textUI != null) { diff --git a/editor.lib/src/org/netbeans/editor/BaseCaret.java b/editor.lib/src/org/netbeans/editor/BaseCaret.java --- a/editor.lib/src/org/netbeans/editor/BaseCaret.java +++ b/editor.lib/src/org/netbeans/editor/BaseCaret.java @@ -78,6 +78,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.Callable; import java.util.logging.Level; import java.util.logging.Logger; import java.util.prefs.PreferenceChangeEvent; @@ -105,12 +106,6 @@ import javax.swing.text.AttributeSet; import javax.swing.text.Position; import javax.swing.text.StyleConstants; -import org.netbeans.api.editor.fold.Fold; -import org.netbeans.api.editor.fold.FoldHierarchy; -import org.netbeans.api.editor.fold.FoldHierarchyEvent; -import org.netbeans.api.editor.fold.FoldHierarchyListener; -import org.netbeans.api.editor.fold.FoldStateChange; -import org.netbeans.api.editor.fold.FoldUtilities; import org.netbeans.api.editor.mimelookup.MimeLookup; import org.netbeans.api.editor.settings.FontColorNames; import org.netbeans.api.editor.settings.FontColorSettings; @@ -136,7 +131,7 @@ public class BaseCaret implements Caret, MouseListener, MouseMotionListener, PropertyChangeListener, DocumentListener, ActionListener, -AtomicLockListener, FoldHierarchyListener { +AtomicLockListener { /** Caret type representing block covering current character */ public static final String BLOCK_CARET = EditorPreferencesDefaults.BLOCK_CARET; // NOI18N @@ -279,7 +274,6 @@ * its relative visual position on the screen. */ private boolean updateAfterFoldHierarchyChange; - private FoldHierarchyListener weakFHListener; /** * Whether at least one typing change occurred during possibly several atomic operations. @@ -530,13 +524,6 @@ EditorUI editorUI = Utilities.getEditorUI(c); editorUI.removePropertyChangeListener(this); - if (weakFHListener != null) { - FoldHierarchy hierarchy = FoldHierarchy.get(c); - if (hierarchy != null) { - hierarchy.removeFoldHierarchyListener(weakFHListener); - } - } - modelChanged(listenDoc, null); } @@ -1231,27 +1218,20 @@ caretPos = doc.createPosition(offset); markPos = doc.createPosition(offset); - FoldHierarchy hierarchy = FoldHierarchy.get(c); - // hook the listener if not already done - if (weakFHListener == null) { - weakFHListener = WeakListeners.create(FoldHierarchyListener.class, this, hierarchy); - hierarchy.addFoldHierarchyListener(weakFHListener); - } - - // Unfold fold - hierarchy.lock(); - try { - Fold collapsed = null; - while (expandFold && (collapsed = FoldUtilities.findCollapsedFold(hierarchy, offset, offset)) != null && collapsed.getStartOffset() < offset && - collapsed.getEndOffset() > offset) { - hierarchy.expand(collapsed); + Callable cc = (Callable)c.getClientProperty("org.netbeans.api.fold.expander"); + if (cc != null) { + // the caretPos/markPos were already called. + // nothing except the document is locked at this moment. + try { + cc.call(); + } catch (Exception ex) { + Exceptions.printStackTrace(ex); } - } finally { - hierarchy.unlock(); } if (rectangularSelection) { setRectangularSelectionToDotAndMark(); } + } catch (BadLocationException e) { throw new IllegalStateException(e.toString()); // setting the caret to wrong position leaves it at current position @@ -1510,37 +1490,25 @@ // Disable drag which would otherwise occur when mouse would be over text c.setDragEnabled(false); // Check possible fold expansion - FoldHierarchy hierarchy = FoldHierarchy.get(c); - Document doc = c.getDocument(); - if (doc instanceof AbstractDocument) { - AbstractDocument adoc = (AbstractDocument) doc; - adoc.readLock(); - try { - hierarchy.lock(); - try { - Fold collapsed = FoldUtilities.findCollapsedFold( - hierarchy, offset, offset); - if (collapsed != null && collapsed.getStartOffset() <= offset - && collapsed.getEndOffset() >= offset) { - hierarchy.expand(collapsed); - } else { - if (selectWordAction == null) { - selectWordAction = ((BaseKit) c.getUI().getEditorKit( - c)).getActionByName(BaseKit.selectWordAction); - } - if (selectWordAction != null) { - selectWordAction.actionPerformed(null); - } - // Select word action selects forward i.e. dot > mark - minSelectionStartOffset = getMark(); - minSelectionEndOffset = getDot(); - } - } finally { - hierarchy.unlock(); + try { + // hack, to get knowledge of possible expansion. Editor depends on Folding, so it's not really possible + // to have Folding depend on BaseCaret (= a cycle). If BaseCaret moves to editor.lib2, this contract + // can be formalized as an interface. + Callable cc = (Callable)c.getClientProperty("org.netbeans.api.fold.expander"); + if (cc == null || !cc.call()) { + if (selectWordAction == null) { + selectWordAction = ((BaseKit) c.getUI().getEditorKit( + c)).getActionByName(BaseKit.selectWordAction); } - } finally { - adoc.readUnlock(); + if (selectWordAction != null) { + selectWordAction.actionPerformed(null); + } + // Select word action selects forward i.e. dot > mark + minSelectionStartOffset = getMark(); + minSelectionEndOffset = getDot(); } + } catch (Exception ex) { + Exceptions.printStackTrace(ex); } break; @@ -1617,37 +1585,48 @@ JTextComponent c = component; if (c != null) { if (isMiddleMouseButtonExt(evt)) { - if (evt.getClickCount() == 1) { - if (c == null) return; + if (evt.getClickCount() == 1) { + if (c == null) { + return; + } Clipboard buffer = getSystemSelection(); - - if (buffer == null) return; + + if (buffer == null) { + return; + } Transferable trans = buffer.getContents(null); - if (trans == null) return; + if (trans == null) { + return; + } - final BaseDocument doc = (BaseDocument)c.getDocument(); - if (doc == null) return; - - final int offset = ((BaseTextUI)c.getUI()).viewToModel(c, - evt.getX(), evt.getY()); + final BaseDocument doc = (BaseDocument) c.getDocument(); + if (doc == null) { + return; + } - try{ - final String pastingString = (String)trans.getTransferData(DataFlavor.stringFlavor); - if (pastingString == null) return; - doc.runAtomicAsUser (new Runnable () { - public @Override void run () { - try { - doc.insertString(offset, pastingString, null); - setDot(offset+pastingString.length()); - } catch( BadLocationException exc ) { - } + final int offset = ((BaseTextUI) c.getUI()).viewToModel(c, + evt.getX(), evt.getY()); + + try { + final String pastingString = (String) trans.getTransferData(DataFlavor.stringFlavor); + if (pastingString == null) { + return; + } + doc.runAtomicAsUser(new Runnable() { + public @Override + void run() { + try { + doc.insertString(offset, pastingString, null); + setDot(offset + pastingString.length()); + } catch (BadLocationException exc) { + } } }); - }catch(UnsupportedFlavorException ufe){ - }catch(IOException ioe){ + } catch (UnsupportedFlavorException ufe) { + } catch (IOException ioe) { } - } + } } } } @@ -2110,67 +2089,6 @@ } } - public @Override void foldHierarchyChanged(FoldHierarchyEvent evt) { - int caretOffset = getDot(); - final int addedFoldCnt = evt.getAddedFoldCount(); - final boolean scrollToView; - LOG.finest("Received fold hierarchy change"); - if (addedFoldCnt > 0) { - FoldHierarchy hierarchy = (FoldHierarchy) evt.getSource(); - Fold collapsed = null; - boolean wasExpanded = false; - while ((collapsed = FoldUtilities.findCollapsedFold(hierarchy, caretOffset, caretOffset)) != null && collapsed.getStartOffset() < caretOffset && - collapsed.getEndOffset() > caretOffset) { - hierarchy.expand(collapsed); - wasExpanded = true; - } - // prevent unneeded scrolling; the user may have scrolled out using mouse already - // so scroll only if the added fold may affect Y axis. Actually it's unclear why - // we should reveal the current position on fold events except when caret is positioned in now-collapsed fold - scrollToView = wasExpanded; - } else { - int startOffset = Integer.MAX_VALUE; - // Set the caret's offset to the end of just collapsed fold if necessary - if (evt.getAffectedStartOffset() <= caretOffset && evt.getAffectedEndOffset() >= caretOffset) { - for (int i = 0; i < evt.getFoldStateChangeCount(); i++) { - FoldStateChange change = evt.getFoldStateChange(i); - if (change.isCollapsedChanged()) { - Fold fold = change.getFold(); - if (fold.isCollapsed() && fold.getStartOffset() <= caretOffset && fold.getEndOffset() >= caretOffset) { - if (fold.getStartOffset() < startOffset) { - startOffset = fold.getStartOffset(); - } - } - } - } - if (startOffset != Integer.MAX_VALUE) { - setDot(startOffset, false); - } - } - scrollToView = false; - } - // Update caret's visual position - // Post the caret update asynchronously since the fold hierarchy is updated before - // the view hierarchy and the views so the dispatchUpdate() could be picking obsolete - // view information. - SwingUtilities.invokeLater(new Runnable() { - public @Override void run() { - LOG.finest("Updating after fold hierarchy change"); - if (component == null) { - return; - } - // see #217867 - Rectangle b = caretBounds; - updateAfterFoldHierarchyChange = b != null; - boolean wasInView = b != null && component.getVisibleRect().intersects(b); - // scroll if: - // a/ a fold was added and the caret was originally in the view - // b/ scrollToView is true (= caret was positioned within a new now collapsed fold) - dispatchUpdate((addedFoldCnt > 1 && wasInView) || scrollToView); - } - }); - } - void scheduleCaretUpdate() { if (!caretUpdatePending) { caretUpdatePending = true; @@ -2304,5 +2222,22 @@ DRAG_SELECTION // Drag is being done (text selection existed at the mouse press) } - + + /** + * Refreshes caret display on the screen. + * Some height or view changes may result in the caret going off the screen. In some cases, this is not desirable, + * as the user's work may be interrupted by e.g. an automatic refresh. This method repositions the view so the + * caret remains visible. + *

+ * The method has two modes: it can reposition the view just if it originally displayed the caret and the caret became + * invisible, and it can scroll the caret into view unconditionally. + * @param retainInView true to scroll only if the caret was visible. False to refresh regardless of visibility. + */ + public void refresh(boolean retainInView) { + Rectangle b = caretBounds; + updateAfterFoldHierarchyChange = b != null; + boolean wasInView = b != null && component.getVisibleRect().intersects(b); + update(!retainInView || wasInView); + } + } diff --git a/editor.lib/src/org/netbeans/editor/Bundle.properties b/editor.lib/src/org/netbeans/editor/Bundle.properties --- a/editor.lib/src/org/netbeans/editor/Bundle.properties +++ b/editor.lib/src/org/netbeans/editor/Bundle.properties @@ -59,14 +59,6 @@ bracket-match=Match Bracket build-popup-menu=Build Popup Menu dump-view-hierarchy=Dump View Hierarchy -collapse-all-folds_menu_text=Co&llapse All -collapse-fold_menu_text=&Collapse Fold -expand-all-folds_menu_text=E&xpand All -expand-fold_menu_text=&Expand Fold -collapse-all-folds=Collapse All -collapse-fold=Collapse Fold -expand-all-folds=Expand All -expand-fold=Expand Fold build-tool-tip=Build Tool Tip caret-backward=Insertion Point Backward caret-begin-line=Insertion Point to Beginning of Text on Line @@ -311,10 +303,6 @@ ## ext.Completion.java ext.Completion.wait=Please wait... -# CodeFoldingSideBar -ACSN_CodeFoldingSideBar=Code folding side bar -ACSD_CodeFoldingSideBar=Code folding side bar shows text folds and allows their collapsing and expanding. - # Format Action Format_in_progress=Formatting text, please wait... diff --git a/editor.lib/src/org/netbeans/editor/CodeFoldingSideBar.java b/editor.lib/src/org/netbeans/editor/CodeFoldingSideBar.java deleted file mode 100644 --- a/editor.lib/src/org/netbeans/editor/CodeFoldingSideBar.java +++ /dev/null @@ -1,1472 +0,0 @@ -/* - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. - * - * Copyright 1997-2010 Oracle and/or its affiliates. All rights reserved. - * - * Oracle and Java are registered trademarks of Oracle and/or its affiliates. - * Other names may be trademarks of their respective owners. - * - * The contents of this file are subject to the terms of either the GNU - * General Public License Version 2 only ("GPL") or the Common - * Development and Distribution License("CDDL") (collectively, the - * "License"). You may not use this file except in compliance with the - * License. You can obtain a copy of the License at - * http://www.netbeans.org/cddl-gplv2.html - * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the - * specific language governing permissions and limitations under the - * License. When distributing the software, include this License Header - * Notice in each file and include the License file at - * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the GPL Version 2 section of the License file that - * accompanied this code. If applicable, add the following below the - * License Header, with the fields enclosed by brackets [] replaced by - * your own identifying information: - * "Portions Copyrighted [year] [name of copyright owner]" - * - * Contributor(s): - * - * The Original Software is NetBeans. The Initial Developer of the Original - * Software is Sun Microsystems, Inc. Portions Copyright 1997-2007 Sun - * Microsystems, Inc. All Rights Reserved. - * - * If you wish your version of this file to be governed by only the CDDL - * or only the GPL Version 2, indicate your decision by adding - * "[Contributor] elects to include this software in this distribution - * under the [CDDL or GPL Version 2] license." If you do not indicate a - * single choice of license, a recipient has the option to distribute - * your version of this file under either the CDDL, the GPL Version 2 or - * to extend the choice of license to its licensees as provided above. - * However, if you add GPL Version 2 code and therefore, elected the GPL - * Version 2 license, then the option applies only if the new code is - * made subject to such option by the copyright holder. - */ - -package org.netbeans.editor; - -import java.awt.BasicStroke; -import java.awt.Color; -import java.awt.Dimension; -import java.awt.Font; -import java.awt.FontMetrics; -import java.awt.Graphics; -import java.awt.Graphics2D; -import java.awt.Point; -import java.awt.Rectangle; -import java.awt.Stroke; -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.NavigableMap; -import java.util.TreeMap; -import java.util.logging.Level; -import java.util.logging.Logger; -import java.util.prefs.PreferenceChangeEvent; -import java.util.prefs.PreferenceChangeListener; -import java.util.prefs.Preferences; -import javax.accessibility.Accessible; -import javax.accessibility.AccessibleContext; -import javax.accessibility.AccessibleRole; -import javax.swing.JComponent; -import javax.swing.SwingUtilities; -import javax.swing.event.DocumentEvent; -import javax.swing.event.DocumentListener; -import javax.swing.text.AbstractDocument; -import javax.swing.text.AttributeSet; -import javax.swing.text.BadLocationException; -import javax.swing.text.Document; -import javax.swing.text.JTextComponent; -import javax.swing.text.View; -import org.netbeans.api.editor.fold.Fold; -import org.netbeans.api.editor.fold.FoldHierarchy; -import org.netbeans.api.editor.fold.FoldHierarchyEvent; -import org.netbeans.api.editor.fold.FoldHierarchyListener; -import org.netbeans.api.editor.fold.FoldUtilities; -import org.netbeans.api.editor.mimelookup.MimeLookup; -import org.netbeans.api.editor.settings.AttributesUtilities; -import org.netbeans.api.editor.settings.FontColorNames; -import org.netbeans.api.editor.settings.FontColorSettings; -import org.netbeans.api.editor.settings.SimpleValueNames; -import org.netbeans.modules.editor.lib2.EditorPreferencesDefaults; -import org.netbeans.modules.editor.lib.SettingsConversions; -import org.netbeans.modules.editor.lib2.view.LockedViewHierarchy; -import org.netbeans.modules.editor.lib2.view.ParagraphViewDescriptor; -import org.netbeans.modules.editor.lib2.view.ViewHierarchy; -import org.netbeans.modules.editor.lib2.view.ViewHierarchyEvent; -import org.netbeans.modules.editor.lib2.view.ViewHierarchyListener; -import org.openide.util.Lookup; -import org.openide.util.LookupEvent; -import org.openide.util.LookupListener; -import org.openide.util.NbBundle; -import org.openide.util.WeakListeners; - -/** - * Code Folding Side Bar. Component responsible for drawing folding signs and responding - * on user fold/unfold action. - * - * @author Martin Roskanin - */ -public class CodeFoldingSideBar extends JComponent implements Accessible { - - private static final Logger LOG = Logger.getLogger(CodeFoldingSideBar.class.getName()); - - /** This field should be treated as final. Subclasses are forbidden to change it. - * @deprecated Without any replacement. - */ - protected Color backColor; - /** This field should be treated as final. Subclasses are forbidden to change it. - * @deprecated Without any replacement. - */ - protected Color foreColor; - /** This field should be treated as final. Subclasses are forbidden to change it. - * @deprecated Without any replacement. - */ - protected Font font; - - /** This field should be treated as final. Subclasses are forbidden to change it. */ - protected /*final*/ JTextComponent component; - private volatile AttributeSet attribs; - private Lookup.Result fcsLookupResult; - private final LookupListener fcsTracker = new LookupListener() { - public void resultChanged(LookupEvent ev) { - attribs = null; - SwingUtilities.invokeLater(new Runnable() { - public void run() { - //EMI: This is needed as maybe the DEFAULT_COLORING is changed, the font is different - // and while getMarkSize() is used in paint() and will make the artifacts bigger, - // the component itself will be the same size and it must be changed. - // See http://www.netbeans.org/issues/show_bug.cgi?id=153316 - updatePreferredSize(); - CodeFoldingSideBar.this.repaint(); - } - }); - } - }; - private final Listener listener = new Listener(); - - private boolean enabled = false; - - protected List visibleMarks = new ArrayList(); - - /** - * Mouse moved point, possibly {@code null}. Set from mouse-moved, mouse-entered - * handlers, so that painting will paint this fold in bold. -1, if mouse is not - * in the sidebar region. The value is used to compute highlighted portions of the - * folding outline. - */ - private int mousePoint = -1; - - /** - * if true, the {@link #mousePoint} has been already used to make a PaintInfo active. - * The flag is tested by {@link #traverseForward} and {@link #traverseBackward} after children - * of the current fold are processed and cleared if the {@link #mousePoint} falls to the fold area - - * fields of PaintInfo are set accordingly. - * It's also used to compute (current) mouseBoundary, so mouse movement does not trigger - * refreshes eagerly - */ - private boolean mousePointConsumed; - - /** - * Boundaries of the current area under the mouse. Can be eiher the span of the - * current fold (or part of it), or the span not occupied by any fold. Serves as an optimization - * for mouse handler, which does not trigger painting (refresh) unless mouse - * leaves this region. - */ - private Rectangle mouseBoundary; - - /** - * Y-end of the nearest fold that ends above the {@link #mousePoint}. Undefined if mousePoint is null. - * These two variables are initialized at each level of folds, and help to compute {@link #mouseBoundary} for - * the case the mousePointer is OUTSIDE all children (or outside all folds). - */ - private int lowestAboveMouse = -1; - - /** - * Y-begin of the nearest fold, which starts below the {@link #mousePoint}. Undefined if mousePoint is null - */ - private int topmostBelowMouse = Integer.MAX_VALUE; - - /** Paint operations */ - public static final int PAINT_NOOP = 0; - /** - * Normal opening +- marker - */ - public static final int PAINT_MARK = 1; - - /** - * Vertical line - typically at the end of the screen - */ - public static final int PAINT_LINE = 2; - - /** - * End angled line, without a sign - */ - public static final int PAINT_END_MARK = 3; - - /** - * Single-line marker, both start and end - */ - public static final int SINGLE_PAINT_MARK = 4; - - /** - * Marker value for {@link #mousePoint} indicating that mouse is outside the Component. - */ - private static final int NO_MOUSE_POINT = -1; - - /** - * Stroke used to draw inactive (regular) fold outlines. - */ - private static Stroke LINE_DASHED = new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, - 1f, new float[] { 1f, 1f }, 0f); - - /** - * Stroke used to draw outlines for 'active' fold - */ - private static final Stroke LINE_BOLD = new BasicStroke(2, BasicStroke.CAP_ROUND, BasicStroke.JOIN_MITER); - - private final Preferences prefs; - private final PreferenceChangeListener prefsListener = new PreferenceChangeListener() { - public void preferenceChange(PreferenceChangeEvent evt) { - String key = evt == null ? null : evt.getKey(); - if (key == null || SimpleValueNames.CODE_FOLDING_ENABLE.equals(key)) { - updateColors(); - - boolean newEnabled = prefs.getBoolean(SimpleValueNames.CODE_FOLDING_ENABLE, EditorPreferencesDefaults.defaultCodeFoldingEnable); - if (enabled != newEnabled) { - enabled = newEnabled; - updatePreferredSize(); - } - } - SettingsConversions.callSettingsChange(CodeFoldingSideBar.this); - } - }; - - private void checkRepaint(ViewHierarchyEvent vhe) { - if (!vhe.isChangeY()) { - // does not obscur sidebar graphics - return; - } - - SwingUtilities.invokeLater(new Runnable() { - public void run() { - updatePreferredSize(); - CodeFoldingSideBar.this.repaint(); - } - }); - } - - /** - * @deprecated Don't use this constructor, it does nothing! - */ - public CodeFoldingSideBar() { - component = null; - prefs = null; - throw new IllegalStateException("Do not use this constructor!"); //NOI18N - } - - public CodeFoldingSideBar(JTextComponent component){ - super(); - this.component = component; - - addMouseListener(listener); - addMouseMotionListener(listener); - - FoldHierarchy foldHierarchy = FoldHierarchy.get(component); - foldHierarchy.addFoldHierarchyListener(WeakListeners.create(FoldHierarchyListener.class, listener, foldHierarchy)); - - Document doc = getDocument(); - doc.addDocumentListener(WeakListeners.document(listener, doc)); - setOpaque(true); - - prefs = MimeLookup.getLookup(org.netbeans.lib.editor.util.swing.DocumentUtilities.getMimeType(component)).lookup(Preferences.class); - prefs.addPreferenceChangeListener(WeakListeners.create(PreferenceChangeListener.class, prefsListener, prefs)); - prefsListener.preferenceChange(null); - - ViewHierarchy.get(component).addViewHierarchyListener(new ViewHierarchyListener() { - - @Override - public void viewHierarchyChanged(ViewHierarchyEvent evt) { - checkRepaint(evt); - } - - }); - } - - private void updatePreferredSize() { - if (enabled) { - setPreferredSize(new Dimension(getColoring().getFont().getSize(), component.getHeight())); - setMaximumSize(new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE)); - }else{ - setPreferredSize(new Dimension(0,0)); - setMaximumSize(new Dimension(0,0)); - } - revalidate(); - } - - private void updateColors() { - Coloring c = getColoring(); - this.backColor = c.getBackColor(); - this.foreColor = c.getForeColor(); - this.font = c.getFont(); - } - - /** - * This method should be treated as final. Subclasses are forbidden to override it. - * @return The background color used for painting this component. - * @deprecated Without any replacement. - */ - protected Color getBackColor() { - if (backColor == null) { - updateColors(); - } - return backColor; - } - - /** - * This method should be treated as final. Subclasses are forbidden to override it. - * @return The foreground color used for painting this component. - * @deprecated Without any replacement. - */ - protected Color getForeColor() { - if (foreColor == null) { - updateColors(); - } - return foreColor; - } - - /** - * This method should be treated as final. Subclasses are forbidden to override it. - * @return The font used for painting this component. - * @deprecated Without any replacement. - */ - protected Font getColoringFont() { - if (font == null) { - updateColors(); - } - return font; - } - - // overriding due to issue #60304 - public @Override void update(Graphics g) { - } - - protected void collectPaintInfos( - View rootView, Fold fold, Map map, int level, int startIndex, int endIndex - ) throws BadLocationException { - //never called - } - - /** - * Adjust lowest/topmost boundaries from the Fold range y1-y2. - * @param y1 - * @param y2 - * @param level - */ - private void setMouseBoundaries(int y1, int y2, int level) { - if (!hasMousePoint() || mousePointConsumed) { - return; - } - int y = mousePoint; - if (y2 < y && lowestAboveMouse < y2) { - LOG.log(Level.FINEST, "lowestAbove at {1}: {0}", new Object[] { y2, level }); - lowestAboveMouse = y2; - } - if (y1 > y && topmostBelowMouse > y1) { - LOG.log(Level.FINEST, "topmostBelow at {1}: {0}", new Object[] { y1, level }); - topmostBelowMouse = y1; - } - } - - /* - * Even collapsed fold MAY contain a continuation line, IF one of folds on the same line is NOT collapsed. Such a fold should - * then visually span multiple lines && be marked as collapsed. - */ - - protected List getPaintInfo(Rectangle clip) throws BadLocationException { - javax.swing.plaf.TextUI textUI = component.getUI(); - if (!(textUI instanceof BaseTextUI)) { - return Collections.emptyList(); - } - BaseTextUI baseTextUI = (BaseTextUI)textUI; - BaseDocument bdoc = Utilities.getDocument(component); - if (bdoc == null) { - return Collections.emptyList(); - } - mousePointConsumed = false; - mouseBoundary = null; - topmostBelowMouse = Integer.MAX_VALUE; - lowestAboveMouse = -1; - bdoc.readLock(); - try { - int startPos = baseTextUI.getPosFromY(clip.y); - int endPos = baseTextUI.viewToModel(component, Short.MAX_VALUE / 2, clip.y + clip.height); - - if (startPos < 0 || endPos < 0) { - // editor window is not properly sized yet; return no infos - return Collections.emptyList(); - } - - // #218282: if the view hierarchy is not yet updated, the Y coordinate may map to an incorrect offset outside - // the document. - int docLen = bdoc.getLength(); - if (startPos >= docLen || endPos > docLen) { - return Collections.emptyList(); - } - - startPos = Utilities.getRowStart(bdoc, startPos); - endPos = Utilities.getRowEnd(bdoc, endPos); - - FoldHierarchy hierarchy = FoldHierarchy.get(component); - hierarchy.lock(); - try { - View rootView = Utilities.getDocumentView(component); - if (rootView != null) { - Object [] arr = getFoldList(hierarchy.getRootFold(), startPos, endPos); - @SuppressWarnings("unchecked") - List foldList = (List) arr[0]; - int idxOfFirstFoldStartingInsideClip = (Integer) arr[1]; - - /* - * Note: - * - * The Map is keyed by Y-VISUAL position of the fold mark, not the textual offset of line start. - * This is because several folds may occupy the same line, while only one + sign is displayed, - * and affect the last fold in the row. - */ - NavigableMap map = new TreeMap(); - // search backwards - for(int i = idxOfFirstFoldStartingInsideClip - 1; i >= 0; i--) { - Fold fold = foldList.get(i); - if (!traverseBackwards(fold, bdoc, baseTextUI, startPos, endPos, 0, map)) { - break; - } - } - - // search forward - for(int i = idxOfFirstFoldStartingInsideClip; i < foldList.size(); i++) { - Fold fold = foldList.get(i); - if (!traverseForward(fold, bdoc, baseTextUI, startPos, endPos, 0, map)) { - break; - } - } - - if (map.isEmpty() && foldList.size() > 0) { - assert foldList.size() == 1; - PaintInfo pi = new PaintInfo(PAINT_LINE, 0, clip.y, clip.height, -1, -1); - mouseBoundary = new Rectangle(0, 0, 0, clip.height); - LOG.log(Level.FINEST, "Mouse boundary for full side line set to: {0}", mouseBoundary); - if (hasMousePoint()) { - pi.markActive(true, true, true); - } - return Collections.singletonList(pi); - } else { - if (mouseBoundary == null) { - mouseBoundary = makeMouseBoundary(clip.y, clip.y + clip.height); - LOG.log(Level.FINEST, "Mouse boundary not set, defaulting to: {0}", mouseBoundary); - } - return new ArrayList(map.values()); - } - } else { - return Collections.emptyList(); - } - } finally { - hierarchy.unlock(); - } - } finally { - bdoc.readUnlock(); - } - } - - /** - * Adds a paint info to the map. If a paintinfo already exists, it merges - * the structures, so the painting process can just follow the instructions. - * - * @param infos - * @param yOffset - * @param nextInfo - */ - private void addPaintInfo(Map infos, int yOffset, PaintInfo nextInfo) { - PaintInfo prevInfo = infos.get(yOffset); - nextInfo.mergeWith(prevInfo); - infos.put(yOffset, nextInfo); - } - - private boolean traverseForward(Fold f, BaseDocument doc, BaseTextUI btui, int lowerBoundary, int upperBoundary,int level, NavigableMap infos) throws BadLocationException { -// System.out.println("~~~ traverseForward<" + lowerBoundary + ", " + upperBoundary -// + ">: fold=<" + f.getStartOffset() + ", " + f.getEndOffset() + "> " -// + (f.getStartOffset() > upperBoundary ? ", f.gSO > uB" : "") -// + ", level=" + level); - - if (f.getStartOffset() > upperBoundary) { - return false; - } - - int lineStartOffset1 = Utilities.getRowStart(doc, f.getStartOffset()); - int lineStartOffset2 = Utilities.getRowStart(doc, f.getEndOffset()); - int y1 = btui.getYFromPos(lineStartOffset1); - int h = btui.getEditorUI().getLineHeight(); - int y2 = btui.getYFromPos(lineStartOffset2); - - // the 'active' flags can be set only after children are processed; highlights - // correspond to the innermost expanded child. - boolean activeMark = false; - boolean activeIn = false; - boolean activeOut = false; - PaintInfo spi; - boolean activated; - - if (y1 == y2) { - // whole fold is on a single line - spi = new PaintInfo(SINGLE_PAINT_MARK, level, y1, h, f.isCollapsed(), lineStartOffset1, lineStartOffset2); - if (activated = isActivated(y1, y1 + h)) { - activeMark = true; - } - addPaintInfo(infos, y1, spi); - } else { - // fold spans multiple lines - spi = new PaintInfo(PAINT_MARK, level, y1, h, f.isCollapsed(), lineStartOffset1, lineStartOffset2); - if (activated = isActivated(y1, y2 + h / 2)) { - activeMark = true; - activeOut = true; - } - addPaintInfo(infos, y1, spi); - } - - setMouseBoundaries(y1, y2 + h / 2, level); - - // Handle end mark after possible inner folds were processed because - // otherwise if there would be two nested folds both ending at the same line - // then the end mark for outer one would be replaced by an end mark for inner one - // (same key in infos map) and the painting code would continue to paint line marking a fold - // until next fold is reached (or end of doc). - PaintInfo epi = null; - if (y1 != y2 && !f.isCollapsed() && f.getEndOffset() <= upperBoundary) { - epi = new PaintInfo(PAINT_END_MARK, level, y2, h, lineStartOffset1, lineStartOffset2); - addPaintInfo(infos, y2, epi); - } - - // save the topmost/lowest information, reset for child processing - int topmost = topmostBelowMouse; - int lowest = lowestAboveMouse; - topmostBelowMouse = y2 + h / 2; - lowestAboveMouse = y1; - - try { - if (!f.isCollapsed()) { - Object [] arr = getFoldList(f, lowerBoundary, upperBoundary); - @SuppressWarnings("unchecked") - List foldList = (List) arr[0]; - int idxOfFirstFoldStartingInsideClip = (Integer) arr[1]; - - // search backwards - for(int i = idxOfFirstFoldStartingInsideClip - 1; i >= 0; i--) { - Fold fold = foldList.get(i); - if (!traverseBackwards(fold, doc, btui, lowerBoundary, upperBoundary, level + 1, infos)) { - break; - } - } - - // search forward - for(int i = idxOfFirstFoldStartingInsideClip; i < foldList.size(); i++) { - Fold fold = foldList.get(i); - if (!traverseForward(fold, doc, btui, lowerBoundary, upperBoundary, level + 1, infos)) { - return false; - } - } - } - if (!mousePointConsumed && activated) { - mousePointConsumed = true; - mouseBoundary = makeMouseBoundary(y1, y2 + h); - LOG.log(Level.FINEST, "Mouse boundary set to: {0}", mouseBoundary); - spi.markActive(activeMark, activeIn, activeOut); - if (epi != null) { - epi.markActive(true, true, false); - } - markDeepChildrenActive(infos, y1, y2, level); - } - } finally { - topmostBelowMouse = topmost; - lowestAboveMouse = lowest; - } - return true; - } - - /** - * Sets outlines of all children to 'active'. Assuming yFrom and yTo are from-to Y-coordinates of the parent - * fold, it finds all nested folds (folds, which are in between yFrom and yTo) and changes their in/out lines - * as active. - * The method returns Y start coordinate of the 1st child found. - * - * @param infos fold infos collected so far - * @param yFrom upper Y-coordinate of the parent fold - * @param yTo lower Y-coordinate of the parent fold - * @param level level of the parent fold - * @return Y-coordinate of the 1st child. - */ - private int markDeepChildrenActive(NavigableMap infos, int yFrom, int yTo, int level) { - int result = Integer.MAX_VALUE; - Map m = infos.subMap(yFrom, yTo); - for (Map.Entry me : m.entrySet()) { - PaintInfo pi = me.getValue(); - int y = pi.getPaintY(); - if (y > yFrom && y < yTo) { - if (LOG.isLoggable(Level.FINEST)) { - LOG.log(Level.FINEST, "Marking chind as active: {0}", pi); - } - pi.markActive(false, true, true); - if (y < result) { - y = result; - } - } - } - return result; - } - - /** - * Returns stroke appropriate for painting (in)active outlines - * @param s the default stroke - * @param active true for active outlines - * @return value of 's' or a Stroke which should be used to paint the outline. - */ - private static Stroke getStroke(Stroke s, boolean active) { - if (active) { - return LINE_BOLD; - } else { - return s; - } - } - - private boolean traverseBackwards(Fold f, BaseDocument doc, BaseTextUI btui, int lowerBoundary, int upperBoundary, int level, NavigableMap infos) throws BadLocationException { -// System.out.println("~~~ traverseBackwards<" + lowerBoundary + ", " + upperBoundary -// + ">: fold=<" + f.getStartOffset() + ", " + f.getEndOffset() + "> " -// + (f.getEndOffset() < lowerBoundary ? ", f.gEO < lB" : "") -// + ", level=" + level); - - if (f.getEndOffset() < lowerBoundary) { - return false; - } - - int lineStartOffset1 = Utilities.getRowStart(doc, f.getStartOffset()); - int lineStartOffset2 = Utilities.getRowStart(doc, f.getEndOffset()); - int h = btui.getEditorUI().getLineHeight(); - - boolean activeMark = false; - boolean activeIn = false; - boolean activeOut = false; - PaintInfo spi = null; - PaintInfo epi = null; - boolean activated = false; - int y1 = 0; - int y2 = 0; - - if (lineStartOffset1 == lineStartOffset2) { - // whole fold is on a single line - y2 = y1 = btui.getYFromPos(lineStartOffset1); - spi = new PaintInfo(SINGLE_PAINT_MARK, level, y1, h, f.isCollapsed(), lineStartOffset1, lineStartOffset1); - if (activated = isActivated(y1, y1 + h)) { - activeMark = true; - } - addPaintInfo(infos, y1, spi); - } else { - y2 = btui.getYFromPos(lineStartOffset2); - // fold spans multiple lines - y1 = btui.getYFromPos(lineStartOffset1); - activated = isActivated(y1, y2 + h / 2); - if (f.getStartOffset() >= upperBoundary) { - spi = new PaintInfo(PAINT_MARK, level, y1, h, f.isCollapsed(), lineStartOffset1, lineStartOffset2); - if (activated) { - activeMark = true; - activeOut = true; - } - addPaintInfo(infos, y1, spi); - } - - if (!f.isCollapsed() && f.getEndOffset() <= upperBoundary) { - activated |= isActivated(y1, y2 + h / 2); - epi = new PaintInfo(PAINT_END_MARK, level, y2, h, lineStartOffset1, lineStartOffset2); - addPaintInfo(infos, y2, epi); - } - } - - setMouseBoundaries(y1, y2 + h / 2, level); - - // save the topmost/lowest information, reset for child processing - int topmost = topmostBelowMouse; - int lowest = lowestAboveMouse; - topmostBelowMouse = y2 + h /2; - lowestAboveMouse = y1; - - try { - if (!f.isCollapsed()) { - Object [] arr = getFoldList(f, lowerBoundary, upperBoundary); - @SuppressWarnings("unchecked") - List foldList = (List) arr[0]; - int idxOfFirstFoldStartingInsideClip = (Integer) arr[1]; - - // search backwards - for(int i = idxOfFirstFoldStartingInsideClip - 1; i >= 0; i--) { - Fold fold = foldList.get(i); - if (!traverseBackwards(fold, doc, btui, lowerBoundary, upperBoundary, level + 1, infos)) { - return false; - } - } - - // search forward - for(int i = idxOfFirstFoldStartingInsideClip; i < foldList.size(); i++) { - Fold fold = foldList.get(i); - if (!traverseForward(fold, doc, btui, lowerBoundary, upperBoundary, level + 1, infos)) { - break; - } - } - } - if (!mousePointConsumed && activated) { - mousePointConsumed = true; - mouseBoundary = makeMouseBoundary(y1, y2 + h); - LOG.log(Level.FINEST, "Mouse boundary set to: {0}", mouseBoundary); - if (spi != null) { - spi.markActive(activeMark, activeIn, activeOut); - } - if (epi != null) { - epi.markActive(true, true, false); - } - int lowestChild = markDeepChildrenActive(infos, y1, y2, level); - if (lowestChild < Integer.MAX_VALUE && lineStartOffset1 < upperBoundary) { - // the fold starts above the screen clip region, and is 'activated'. We need to setup instructions to draw activated line up to the - // 1st child marker. - epi = new PaintInfo(PAINT_LINE, level, y1, y2 - y1, false, lineStartOffset1, lineStartOffset2); - epi.markActive(true, true, false); - addPaintInfo(infos, y1, epi); - } - } - } finally { - topmostBelowMouse = topmost; - lowestAboveMouse = lowest; - } - return true; - } - - private Rectangle makeMouseBoundary(int y1, int y2) { - if (!hasMousePoint()) { - return null; - } - if (topmostBelowMouse < Integer.MAX_VALUE) { - y2 = topmostBelowMouse; - } - if (lowestAboveMouse > -1) { - y1 = lowestAboveMouse; - } - return new Rectangle(0, y1, 0, y2 - y1); - } - - protected EditorUI getEditorUI(){ - return Utilities.getEditorUI(component); - } - - protected Document getDocument(){ - return component.getDocument(); - } - - - private Fold getLastLineFold(FoldHierarchy hierarchy, int rowStart, int rowEnd, boolean shift){ - Fold fold = FoldUtilities.findNearestFold(hierarchy, rowStart); - Fold prevFold = fold; - while (fold != null && fold.getStartOffset()= startY && mouseY <= nextY) { - LOG.log(Level.FINEST, "Starting line clicked, ignoring. MouseY={0}, startY={1}, nextY={2}", - new Object[] { mouseY, startY, nextY }); - return; - } - - startY = textUI.getYFromPos(endOffset); - nextLineOffset = Utilities.getRowStart(bdoc, endOffset, 1); - nextY = textUI.getYFromPos(nextLineOffset); - - if (mouseY >= startY && mouseY <= nextY) { - // the mouse can be positioned above the marker (the fold found above), or - // below it; in that case, the immediate enclosing fold should be used - should be the fold - // that corresponds to the nextLineOffset, if any - int h2 = (startY + nextY) / 2; - if (mouseY >= h2) { - Fold f2 = f; - - f = FoldUtilities.findOffsetFold(hierarchy, nextLineOffset); - if (f == null) { - // fold does not exist for the position below end-of-fold indicator - return; - } - } - - } - - LOG.log(Level.FINEST, "Collapsing fold: {0}", f); - hierarchy.collapse(f); - } finally { - hierarchy.unlock(); - } - } finally { - bdoc.readUnlock(); - } - } - - private void performAction(final Mark mark, final boolean shiftFold) { - Document doc = component.getDocument(); - doc.render(new Runnable() { - @Override - public void run() { - ViewHierarchy vh = ViewHierarchy.get(component); - LockedViewHierarchy lockedVH = vh.lock(); - try { - int pViewIndex = lockedVH.yToParagraphViewIndex(mark.y + mark.size / 2); - if (pViewIndex >= 0) { - ParagraphViewDescriptor pViewDesc = lockedVH.getParagraphViewDescriptor(pViewIndex); - int pViewStartOffset = pViewDesc.getStartOffset(); - int pViewEndOffset = pViewStartOffset + pViewDesc.getLength(); - // Find corresponding fold - FoldHierarchy foldHierarchy = FoldHierarchy.get(component); - foldHierarchy.lock(); - try { - int rowStart = javax.swing.text.Utilities.getRowStart(component, pViewStartOffset); - int rowEnd = javax.swing.text.Utilities.getRowEnd(component, pViewStartOffset); - Fold clickedFold = getLastLineFold(foldHierarchy, rowStart, rowEnd, shiftFold);//FoldUtilities.findNearestFold(foldHierarchy, viewStartOffset); - if (clickedFold != null && clickedFold.getStartOffset() < pViewEndOffset) { - foldHierarchy.toggle(clickedFold); - } - } catch (BadLocationException ble) { - LOG.log(Level.WARNING, null, ble); - } finally { - foldHierarchy.unlock(); - } - } - } finally { - lockedVH.unlock(); - } - } - }); - } - - protected int getMarkSize(Graphics g){ - if (g != null){ - FontMetrics fm = g.getFontMetrics(getColoring().getFont()); - if (fm != null){ - int ret = fm.getAscent() - fm.getDescent(); - return ret - ret%2; - } - } - return -1; - } - - private boolean hasMousePoint() { - return mousePoint >= 0; - } - - private boolean isActivated(int y1, int y2) { - return hasMousePoint() && - (mousePoint >= y1 && mousePoint < y2); - } - - private void drawFoldLine(Graphics2D g2d, boolean active, int x1, int y1, int x2, int y2) { - Stroke origStroke = g2d.getStroke(); - g2d.setStroke(getStroke(origStroke, active)); - g2d.drawLine(x1, y1, x2, y2); - g2d.setStroke(origStroke); - } - - protected @Override void paintComponent(Graphics g) { - if (!enabled) { - return; - } - - Rectangle clip = getVisibleRect();//g.getClipBounds(); - visibleMarks.clear(); - - Coloring coloring = getColoring(); - g.setColor(coloring.getBackColor()); - g.fillRect(clip.x, clip.y, clip.width, clip.height); - g.setColor(coloring.getForeColor()); - - AbstractDocument adoc = (AbstractDocument)component.getDocument(); - adoc.readLock(); - try { - List ps = getPaintInfo(clip); - Font defFont = coloring.getFont(); - int markSize = getMarkSize(g); - int halfMarkSize = markSize / 2; - int markX = (defFont.getSize() - markSize) / 2; // x position of mark rectangle - int plusGap = (int)Math.round(markSize / 3.8); // distance between mark rectangle vertical side and start/end of minus sign - int lineX = markX + halfMarkSize; // x position of the centre of mark - - LOG.fine("CFSBar: PAINT START ------\n"); - int descent = g.getFontMetrics(defFont).getDescent(); - PaintInfo previousInfo = null; - Graphics2D g2d = (Graphics2D)g; - LOG.log(Level.FINEST, "MousePoint: {0}", mousePoint); - - for(PaintInfo paintInfo : ps) { - boolean isFolded = paintInfo.isCollapsed(); - int y = paintInfo.getPaintY(); - int height = paintInfo.getPaintHeight(); - int markY = y + descent; // y position of mark rectangle - int paintOperation = paintInfo.getPaintOperation(); - - if (previousInfo == null) { - if (paintInfo.hasLineIn()) { - if (LOG.isLoggable(Level.FINE)) { - LOG.fine("prevInfo=NULL; y=" + y + ", PI:" + paintInfo + "\n"); // NOI18N - } - drawFoldLine(g2d, paintInfo.lineInActive, lineX, clip.y, lineX, y); - } - } else { - if (previousInfo.hasLineOut() || paintInfo.hasLineIn()) { - // Draw middle vertical line - int prevY = previousInfo.getPaintY(); - if (LOG.isLoggable(Level.FINE)) { - LOG.log(Level.FINE, "prevInfo={0}; y=" + y + ", PI:" + paintInfo + "\n", previousInfo); // NOI18N - } - drawFoldLine(g2d, previousInfo.lineOutActive || paintInfo.lineInActive, lineX, prevY + previousInfo.getPaintHeight(), lineX, y); - } - } - - if (paintInfo.hasSign()) { - g.drawRect(markX, markY, markSize, markSize); - g.drawLine(plusGap + markX, markY + halfMarkSize, markSize + markX - plusGap, markY + halfMarkSize); - String opStr = (paintOperation == PAINT_MARK) ? "PAINT_MARK" : "SINGLE_PAINT_MARK"; // NOI18N - if (isFolded) { - if (LOG.isLoggable(Level.FINE)) { - LOG.fine(opStr + ": folded; y=" + y + ", PI:" + paintInfo + "\n"); // NOI18N - } - g.drawLine(lineX, markY + plusGap, lineX, markY + markSize - plusGap); - } - if (paintOperation != SINGLE_PAINT_MARK) { - if (LOG.isLoggable(Level.FINE)) { - LOG.fine(opStr + ": non-single; y=" + y + ", PI:" + paintInfo + "\n"); // NOI18N - } - } - if (paintInfo.hasLineIn()) { //[PENDING] - drawFoldLine(g2d, paintInfo.lineInActive, lineX, y, lineX, markY); - } - if (paintInfo.hasLineOut()) { - // This is an error in case there's a next paint info at the same y which is an end mark - // for this mark (it must be cleared explicitly). - drawFoldLine(g2d, paintInfo.lineOutActive, lineX, markY + markSize, lineX, y + height); - } - visibleMarks.add(new Mark(markX, markY, markSize, isFolded)); - - } else if (paintOperation == PAINT_LINE) { - if (LOG.isLoggable(Level.FINE)) { - LOG.fine("PAINT_LINE: y=" + y + ", PI:" + paintInfo + "\n"); // NOI18N - } - // FIXME !! - drawFoldLine(g2d, paintInfo.signActive, lineX, y, lineX, y + height ); - } else if (paintOperation == PAINT_END_MARK) { - if (LOG.isLoggable(Level.FINE)) { - LOG.fine("PAINT_END_MARK: y=" + y + ", PI:" + paintInfo + "\n"); // NOI18N - } - if (previousInfo == null || y != previousInfo.getPaintY()) { - drawFoldLine(g2d, paintInfo.lineInActive, lineX, y, lineX, y + height / 2); - drawFoldLine(g2d, paintInfo.signActive, lineX, y + height / 2, lineX + halfMarkSize, y + height / 2); - if (paintInfo.getInnerLevel() > 0) {//[PENDING] - if (LOG.isLoggable(Level.FINE)) { - LOG.fine(" PAINT middle-line\n"); // NOI18N - } - drawFoldLine(g2d, paintInfo.lineOutActive, lineX, y + height / 2, lineX, y + height); - } - } - } - - previousInfo = paintInfo; - } - - if (previousInfo != null && - (previousInfo.getInnerLevel() > 0 || - (previousInfo.getPaintOperation() == PAINT_MARK && !previousInfo.isCollapsed())) - ) { - drawFoldLine(g2d, previousInfo.lineOutActive, - lineX, previousInfo.getPaintY() + previousInfo.getPaintHeight(), lineX, clip.y + clip.height); - } - - } catch (BadLocationException ble) { - LOG.log(Level.WARNING, null, ble); - } finally { - LOG.fine("CFSBar: PAINT END ------\n\n"); - adoc.readUnlock(); - } - } - - private static Object [] getFoldList(Fold parentFold, int start, int end) { - List ret = new ArrayList(); - - int index = FoldUtilities.findFoldEndIndex(parentFold, start); - int foldCount = parentFold.getFoldCount(); - int idxOfFirstFoldStartingInside = -1; - while (index < foldCount) { - Fold f = parentFold.getFold(index); - if (f.getStartOffset() <= end) { - ret.add(f); - } else { - break; // no more relevant folds - } - if (idxOfFirstFoldStartingInside == -1 && f.getStartOffset() >= start) { - idxOfFirstFoldStartingInside = ret.size() - 1; - } - index++; - } - - return new Object [] { ret, idxOfFirstFoldStartingInside != -1 ? idxOfFirstFoldStartingInside : ret.size() }; - } - - /** - * This class should be never used by other code; will be made private - */ - public class PaintInfo { - - int paintOperation; - /** - * level of the 1st marker on the line - */ - int innerLevel; - - /** - * Y-coordinate of the cell - */ - int paintY; - - /** - * Height of the paint cell - */ - int paintHeight; - - /** - * State of the marker (+/-) - */ - boolean isCollapsed; - - /** - * all markers on the line are collapsed - */ - boolean allCollapsed; - int startOffset; - int endOffset; - /** - * nesting level of the last marker on the line - */ - int outgoingLevel; - - /** - * Force incoming line (from above) to be present - */ - boolean lineIn; - - /** - * Force outgoing line (down from marker) to be present - */ - boolean lineOut; - - /** - * The 'incoming' (upper) line should be painted as active - */ - boolean lineInActive; - - /** - * The 'outgoing' (down) line should be painted as active - */ - boolean lineOutActive; - - /** - * The sign/marker itself should be painted as active - */ - boolean signActive; - - public PaintInfo(int paintOperation, int innerLevel, int paintY, int paintHeight, boolean isCollapsed, int startOffset, int endOffset){ - this.paintOperation = paintOperation; - this.innerLevel = this.outgoingLevel = innerLevel; - this.paintY = paintY; - this.paintHeight = paintHeight; - this.isCollapsed = this.allCollapsed = isCollapsed; - this.startOffset = startOffset; - this.endOffset = endOffset; - - switch (paintOperation) { - case PAINT_MARK: - lineIn = false; - lineOut = true; - outgoingLevel++; - break; - case SINGLE_PAINT_MARK: - lineIn = false; - lineOut = false; - break; - case PAINT_END_MARK: - lineIn = true; - lineOut = false; - isCollapsed = true; - allCollapsed = true; - break; - case PAINT_LINE: - lineIn = lineOut = true; - break; - } - } - - /** - * Sets active flags on inidivual parts of the mark - * @param mark - * @param lineIn - * @param lineOut S - */ - void markActive(boolean mark, boolean lineIn, boolean lineOut) { - this.signActive |= mark; - this.lineInActive |= lineIn; - this.lineOutActive |= lineOut; - } - - boolean hasLineIn() { - return lineIn || innerLevel > 0; - } - - boolean hasLineOut() { - return lineOut || outgoingLevel > 0 || (paintOperation != SINGLE_PAINT_MARK && !isAllCollapsed()); - } - - public PaintInfo(int paintOperation, int innerLevel, int paintY, int paintHeight, int startOffset, int endOffset){ - this(paintOperation, innerLevel, paintY, paintHeight, false, startOffset, endOffset); - } - - public int getPaintOperation(){ - return paintOperation; - } - - public int getInnerLevel(){ - return innerLevel; - } - - public int getPaintY(){ - return paintY; - } - - public int getPaintHeight(){ - return paintHeight; - } - - public boolean isCollapsed(){ - return isCollapsed; - } - - boolean isAllCollapsed() { - return allCollapsed; - } - - public void setPaintOperation(int paintOperation){ - this.paintOperation = paintOperation; - } - - public void setInnerLevel(int innerLevel){ - this.innerLevel = innerLevel; - } - - public @Override String toString(){ - StringBuffer sb = new StringBuffer(""); - if (paintOperation == PAINT_MARK){ - sb.append("PAINT_MARK"); // NOI18N - }else if (paintOperation == PAINT_LINE){ - sb.append("PAINT_LINE"); // NOI18N - }else if (paintOperation == PAINT_END_MARK) { - sb.append("PAINT_END_MARK"); // NOI18N - }else if (paintOperation == SINGLE_PAINT_MARK) { - sb.append("SINGLE_PAINT_MARK"); - } - sb.append(",L:").append(innerLevel).append("/").append(outgoingLevel); // NOI18N - sb.append(',').append(isCollapsed ? "C" : "E"); // NOI18N - sb.append(", start=").append(startOffset).append(", end=").append(endOffset); - sb.append(", lineIn=").append(lineIn).append(", lineOut=").append(lineOut); - return sb.toString(); - } - - boolean hasSign() { - return paintOperation == PAINT_MARK || paintOperation == SINGLE_PAINT_MARK; - } - - - void mergeWith(PaintInfo prevInfo) { - if (prevInfo == null) { - return; - } - - int operation = this.paintOperation; - boolean lineIn = prevInfo.lineIn; - boolean lineOut = prevInfo.lineOut; - - LOG.log(Level.FINE, "Merging {0} with {1}: ", new Object[] { this, prevInfo }); - if (prevInfo.getPaintOperation() == PAINT_END_MARK) { - // merge with start|single -> start mark + line-in - lineIn = true; - } else { - operation = PAINT_MARK; - } - - int level1 = Math.min(prevInfo.innerLevel, innerLevel); - int level2 = prevInfo.outgoingLevel; - - if (getPaintOperation() == PAINT_END_MARK - && innerLevel == prevInfo.outgoingLevel) { - // if merging end marker at the last level, update to the new outgoing level - level2 = outgoingLevel; - } else if (!isCollapsed) { - level2 = Math.max(prevInfo.outgoingLevel, outgoingLevel); - } - - if (prevInfo.getInnerLevel() < getInnerLevel()) { - int paintFrom = Math.min(prevInfo.paintY, paintY); - int paintTo = Math.max(prevInfo.paintY + prevInfo.paintHeight, paintY + paintHeight); - // at least one collapsed -> paint plus sign - boolean collapsed = prevInfo.isCollapsed() || isCollapsed(); - int offsetFrom = Math.min(prevInfo.startOffset, startOffset); - int offsetTo = Math.max(prevInfo.endOffset, endOffset); - - this.paintY = paintFrom; - this.paintHeight = paintTo - paintFrom; - this.isCollapsed = collapsed; - this.startOffset = offsetFrom; - this.endOffset = offsetTo; - } - this.paintOperation = operation; - this.allCollapsed = prevInfo.allCollapsed && allCollapsed; - this.innerLevel = level1; - this.outgoingLevel = level2; - this.lineIn |= lineIn; - this.lineOut |= lineOut; - - this.signActive |= prevInfo.signActive; - this.lineInActive |= prevInfo.lineInActive; - this.lineOutActive |= prevInfo.lineOutActive; - - LOG.log(Level.FINE, "Merged result: {0}", this); - } - } - - /** Keeps info of visible folding mark */ - public class Mark{ - public int x; - public int y; - public int size; - public boolean isFolded; - - public Mark(int x, int y, int size, boolean isFolded){ - this.x = x; - this.y = y; - this.size = size; - this.isFolded = isFolded; - } - } - - private final class Listener extends MouseAdapter implements FoldHierarchyListener, DocumentListener, Runnable { - - public Listener(){ - } - - // -------------------------------------------------------------------- - // FoldHierarchyListener implementation - // -------------------------------------------------------------------- - - public void foldHierarchyChanged(FoldHierarchyEvent evt) { - refresh(); - } - - // -------------------------------------------------------------------- - // DocumentListener implementation - // -------------------------------------------------------------------- - - public void insertUpdate(DocumentEvent evt) { - if (!(evt instanceof BaseDocumentEvent)) return; - - BaseDocumentEvent bevt = (BaseDocumentEvent)evt; - if (bevt.getLFCount() > 0) { // one or more lines inserted - refresh(); - } - } - - public void removeUpdate(DocumentEvent evt) { - if (!(evt instanceof BaseDocumentEvent)) return; - - BaseDocumentEvent bevt = (BaseDocumentEvent)evt; - if (bevt.getLFCount() > 0) { // one or more lines removed - refresh(); - } - } - - public void changedUpdate(DocumentEvent evt) { - } - - // -------------------------------------------------------------------- - // MouseListener implementation - // -------------------------------------------------------------------- - - @Override - public void mousePressed (MouseEvent e) { - Mark mark = getClickedMark(e); - if (mark!=null){ - e.consume(); - performAction(mark, (e.getModifiersEx() & MouseEvent.CTRL_DOWN_MASK) > 0); - } - } - - @Override - public void mouseClicked(MouseEvent e) { - // #102288 - missing event consuming caused quick doubleclicks to break - // fold expanding/collapsing and move caret to the particular line - if (e.getClickCount() > 1) { - LOG.log(Level.FINEST, "Mouse {0}click at {1}", new Object[] { e.getClickCount(), e.getY()}); - Mark mark = getClickedMark(e); - try { - performActionAt(mark, e.getY()); - } catch (BadLocationException ex) { - LOG.log(Level.WARNING, "Error during fold expansion using sideline", ex); - } - } else { - e.consume(); - } - } - - private void refreshIfMouseOutside(Point pt) { - mousePoint = (int)pt.getY(); - if (LOG.isLoggable(Level.FINEST)) { - if (mouseBoundary == null) { - LOG.log(Level.FINEST, "Mouse boundary not set, refreshing: {0}", mousePoint); - } else { - LOG.log(Level.FINEST, "Mouse {0} inside known mouse boundary: {1}-{2}", - new Object[] { mousePoint, mouseBoundary.y, mouseBoundary.getMaxY() }); - } - } - if (mouseBoundary == null || mousePoint < mouseBoundary.y || mousePoint > mouseBoundary.getMaxY()) { - refresh(); - } - } - - @Override - public void mouseMoved(MouseEvent e) { - refreshIfMouseOutside(e.getPoint()); - } - - public void mouseEntered(MouseEvent e) { - refreshIfMouseOutside(e.getPoint()); - } - - public void mouseExited(MouseEvent e) { - mousePoint = NO_MOUSE_POINT; - refresh(); - } - - - // -------------------------------------------------------------------- - // private implementation - // -------------------------------------------------------------------- - - private Mark getClickedMark(MouseEvent e){ - if (e == null || !SwingUtilities.isLeftMouseButton(e)) { - return null; - } - - int x = e.getX(); - int y = e.getY(); - for (Mark mark : visibleMarks) { - if (x >= mark.x && x <= (mark.x + mark.size) && y >= mark.y && y <= (mark.y + mark.size)) { - return mark; - } - } - return null; - } - - private void refresh() { - SwingUtilities.invokeLater(this); - } - - @Override - public void run() { - repaint(); - } - } // End of Listener class - - @Override - public AccessibleContext getAccessibleContext() { - if (accessibleContext == null) { - accessibleContext = new AccessibleJComponent() { - public @Override AccessibleRole getAccessibleRole() { - return AccessibleRole.PANEL; - } - }; - accessibleContext.setAccessibleName(NbBundle.getMessage(CodeFoldingSideBar.class, "ACSN_CodeFoldingSideBar")); //NOI18N - accessibleContext.setAccessibleDescription(NbBundle.getMessage(CodeFoldingSideBar.class, "ACSD_CodeFoldingSideBar")); //NOI18N - } - return accessibleContext; - } - - private Coloring getColoring() { - if (attribs == null) { - if (fcsLookupResult == null) { - fcsLookupResult = MimeLookup.getLookup(org.netbeans.lib.editor.util.swing.DocumentUtilities.getMimeType(component)) - .lookupResult(FontColorSettings.class); - fcsLookupResult.addLookupListener(WeakListeners.create(LookupListener.class, fcsTracker, fcsLookupResult)); - } - - FontColorSettings fcs = fcsLookupResult.allInstances().iterator().next(); - AttributeSet attr = fcs.getFontColors(FontColorNames.CODE_FOLDING_BAR_COLORING); - if (attr == null) { - attr = fcs.getFontColors(FontColorNames.DEFAULT_COLORING); - } else { - attr = AttributesUtilities.createComposite(attr, fcs.getFontColors(FontColorNames.DEFAULT_COLORING)); - } - attribs = attr; - } - return Coloring.fromAttributeSet(attribs); - } - -} diff --git a/editor.lib/src/org/netbeans/editor/GlyphGutter.java b/editor.lib/src/org/netbeans/editor/GlyphGutter.java --- a/editor.lib/src/org/netbeans/editor/GlyphGutter.java +++ b/editor.lib/src/org/netbeans/editor/GlyphGutter.java @@ -88,9 +88,6 @@ import javax.swing.text.Element; import javax.swing.text.JTextComponent; import javax.swing.text.View; -import org.netbeans.api.editor.fold.FoldHierarchy; -import org.netbeans.api.editor.fold.FoldHierarchyEvent; -import org.netbeans.api.editor.fold.FoldHierarchyListener; import org.netbeans.api.editor.mimelookup.MimeLookup; import org.netbeans.api.editor.settings.EditorStyleConstants; import org.netbeans.api.editor.settings.FontColorNames; @@ -182,9 +179,7 @@ /** Property change listener on AnnotationTypes changes */ private PropertyChangeListener annoTypesListener; private PropertyChangeListener editorUIListener; - private GlyphGutter.GlyphGutterFoldHierarchyListener glyphGutterFoldHierarchyListener; private GutterMouseListener gutterMouseListener; - private FoldHierarchy foldHierarchy; private ColoringMap coloringMap; private final PropertyChangeListener coloringMapListener = new PropertyChangeListener() { @@ -234,9 +229,6 @@ init(); update(); setMaximumSize(new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE)); - foldHierarchy = FoldHierarchy.get(eui.getComponent()); - glyphGutterFoldHierarchyListener = new GlyphGutterFoldHierarchyListener(); - foldHierarchy.addFoldHierarchyListener(glyphGutterFoldHierarchyListener); editorUIListener = new EditorUIListener(); eui.addPropertyChangeListener(editorUIListener); eui.getComponent().addPropertyChangeListener(editorUIListener); @@ -1096,24 +1088,6 @@ } - class GlyphGutterFoldHierarchyListener implements FoldHierarchyListener, Runnable { - - public GlyphGutterFoldHierarchyListener(){ - } - - public @Override void foldHierarchyChanged(FoldHierarchyEvent evt) { - // Do not react to fold changes since fold expanding/collapsing should trigger - // corresponding view hierarchy changes covered by view hierarchy listener. - -// SwingUtilities.invokeLater(this); - } - - @Override - public void run() { - repaint(); - } - } - /** Listening to EditorUI to properly deinstall attached listeners */ class EditorUIListener implements PropertyChangeListener{ public @Override void propertyChange (PropertyChangeEvent evt) { @@ -1126,7 +1100,6 @@ ((JTextComponent) evt.getOldValue()).removePropertyChangeListener(this); } annos.removeAnnotationsListener(GlyphGutter.this); - foldHierarchy.removeFoldHierarchyListener(glyphGutterFoldHierarchyListener); if (gutterMouseListener!=null){ removeMouseListener(gutterMouseListener); removeMouseMotionListener(gutterMouseListener); @@ -1134,8 +1107,6 @@ if (annoTypesListener !=null){ AnnotationTypes.getTypes().removePropertyChangeListener(annoTypesListener); } - foldHierarchy.removeFoldHierarchyListener(glyphGutterFoldHierarchyListener); - foldHierarchy = null; editorUI = null; annos = null; } diff --git a/editor.lib/src/org/netbeans/modules/editor/lib/drawing/DrawEngineDocView.java b/editor.lib/src/org/netbeans/modules/editor/lib/drawing/DrawEngineDocView.java --- a/editor.lib/src/org/netbeans/modules/editor/lib/drawing/DrawEngineDocView.java +++ b/editor.lib/src/org/netbeans/modules/editor/lib/drawing/DrawEngineDocView.java @@ -48,20 +48,12 @@ import java.awt.Rectangle; import java.awt.Shape; import java.beans.PropertyChangeListener; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; import javax.swing.plaf.TextUI; import javax.swing.text.AbstractDocument; import javax.swing.text.Element; import javax.swing.text.JTextComponent; import javax.swing.text.View; import javax.swing.text.ViewFactory; -import org.netbeans.api.editor.fold.Fold; -import org.netbeans.api.editor.fold.FoldHierarchy; -import org.netbeans.api.editor.fold.FoldUtilities; -import org.netbeans.api.editor.fold.FoldHierarchyEvent; -import org.netbeans.api.editor.fold.FoldHierarchyListener; import org.netbeans.editor.BaseTextUI; import org.netbeans.editor.EditorUI; import org.netbeans.lib.editor.view.GapDocumentView; @@ -73,19 +65,15 @@ * @author Miloslav Metelka */ public class DrawEngineDocView extends GapDocumentView -implements FoldHierarchyListener, PropertyChangeListener { +implements PropertyChangeListener { private static final boolean debugRebuild = Boolean.getBoolean("netbeans.debug.editor.view.rebuild"); // NOI18N - private FoldHierarchy foldHierarchy; /** Editor UI listening to */ private EditorUI editorUI; - private Iterator collapsedFoldIterator; - private Fold collapsedFold; private int collapsedFoldStartOffset; - private int collapsedFoldEndOffset; private boolean collapsedFoldsInPresentViews; @@ -104,8 +92,6 @@ public @Override void setParent(View parent) { if (parent != null) { // start listening JTextComponent component = (JTextComponent)parent.getContainer(); - foldHierarchy = FoldHierarchy.get(component); - foldHierarchy.addFoldHierarchyListener(this); TextUI tui = component.getUI(); if (tui instanceof BaseTextUI){ editorUI = ((BaseTextUI)tui).getEditorUI(); @@ -118,8 +104,6 @@ super.setParent(parent); if (parent == null) { - foldHierarchy.removeFoldHierarchyListener(this); - foldHierarchy = null; if (editorUI!=null){ editorUI.removePropertyChangeListener(this); editorUI = null; @@ -128,111 +112,12 @@ } protected void attachListeners(){ - if (foldHierarchy != null) { - } - } - - private FoldHierarchy getFoldHierarchy() { - return foldHierarchy; } protected @Override boolean useCustomReloadChildren() { return true; } - /** - * Find next collapsed fold in the given offset range. - * @param lastCollapsedFold last collapsed fold returned by this method. - * @param startOffset starting offset of the area in which the collapsed folds - * should be searched. - * @param endOffset ending offset of the area in which the collapsed folds - * should be searched. - */ - protected Fold nextCollapsedFold() { - while (true) { - Fold fold = collapsedFoldIterator.hasNext() ? (Fold)collapsedFoldIterator.next() : null; - - // Check whether the fold is not past the doc - if (fold != null) { - collapsedFoldStartOffset = fold.getStartOffset(); - collapsedFoldEndOffset = fold.getEndOffset(); - /* Ignore the empty folds as they would make up - * no visible view anyway. - * Although the fold hierarchy removes the empty views - * automatically it may happen that the document listener - * that the fold hierarchy attaches may not be notified yet. - */ - if (collapsedFoldStartOffset == collapsedFoldEndOffset) { - if (debugRebuild) { - /*DEBUG*/System.err.println( - "GapBoxView.nextCollapsedFold(): ignored empty fold " // NOI18N - + fold - ); - } - continue; // skip empty fold - } - - if (collapsedFoldEndOffset > getDocument().getLength()) { - /* The fold is past the end of the document. - * If a document is going to be switched in the component - * the view hierarchy may be notified sooner - * than fold hierarchy about that change which - * can lead to this state. - * That fold is ignored together with the rest of the folds - * that would follow it. - */ - fold = null; - } - } - - if (fold != null) { - collapsedFoldsInPresentViews = true; - } - - return fold; - } - } - - /** - * Extra initialization for custom reload of children. - */ - protected void initCustomReloadChildren(FoldHierarchy hierarchy, - int startOffset, int endOffset) { - collapsedFoldIterator = FoldUtilities.collapsedFoldIterator(hierarchy, startOffset, endOffset); - collapsedFold = nextCollapsedFold(); - } - - /** - * Free any resources required for custom reload of children. - */ - protected void finishCustomReloadChildren(FoldHierarchy hierarchy) { - collapsedFoldIterator = null; - collapsedFold = null; - } - - protected @Override void customReloadChildren(int index, int removeLength, int startOffset, int endOffset) { - // if removing all the views reset the flag - if (index == 0 && removeLength == getViewCount()) { - collapsedFoldsInPresentViews = false; // suppose there will be no folds in line views - } - - FoldHierarchy hierarchy = getFoldHierarchy(); - // Assuming the document lock was already acquired - if (hierarchy != null) { - hierarchy.lock(); - try { - initCustomReloadChildren(hierarchy, startOffset, endOffset); - - super.customReloadChildren(index, removeLength, startOffset, endOffset); - - finishCustomReloadChildren(hierarchy); - - } finally { - hierarchy.unlock(); - } - } - } - protected @Override View createCustomView(ViewFactory f, int startOffset, int maxEndOffset, int elementIndex) { if (elementIndex == -1) { @@ -243,94 +128,10 @@ Element elem = getElement(); Element lineElem = elem.getElement(elementIndex); - boolean createCollapsed = (collapsedFold != null); - - if (createCollapsed) { // collapsedFold != null - int lineElemEndOffset = lineElem.getEndOffset(); - createCollapsed = (collapsedFoldStartOffset < lineElemEndOffset); - if (createCollapsed) { // need to find end of collapsed area - Element firstLineElem = lineElem; - List foldAndEndLineElemList = new ArrayList(); - - while (true) { - int _collapsedFoldEndOffset = collapsedFold.getEndOffset(); - // Find line element index of the line in which the collapsed fold ends - while (_collapsedFoldEndOffset > lineElemEndOffset) { - elementIndex++; - lineElem = elem.getElement(elementIndex); - lineElemEndOffset = lineElem.getEndOffset(); - } - - foldAndEndLineElemList.add(collapsedFold); - foldAndEndLineElemList.add(lineElem); - - collapsedFold = nextCollapsedFold(); - - // No more collapsed or next collapsed does not start on current line - if (collapsedFold == null || collapsedFoldStartOffset >= lineElemEndOffset) { - break; - } - } - - // Create the multi-line-view with collapsed fold(s) - view = new FoldMultiLineView(firstLineElem, foldAndEndLineElemList); - } - } - - if (!createCollapsed) { - view = f.create(lineElem); - } - + view = f.create(lineElem); return view; } - public void foldHierarchyChanged(FoldHierarchyEvent evt) { - LockView lockView = LockView.get(this); - lockView.lock(); - try { - layoutLock(); - try { - FoldHierarchy hierarchy = (FoldHierarchy)evt.getSource(); - if (hierarchy.getComponent().getDocument() != lockView.getDocument()) { - // Comonent already has a different document assigned - // so this view will be abandoned anyway => do not rebuild - // the current chilren because of this change - return; - } - - boolean rebuildViews = true; - int affectedStartOffset = evt.getAffectedStartOffset(); - int affectedEndOffset = evt.getAffectedEndOffset(); - - // Check whether it is not a case when there were - // no collapsed folds before and no collapsed folds now - if (!collapsedFoldsInPresentViews) { // no collapsed folds previously - // TODO Could Integer.MAX_VALUE be used? - if (FoldUtilities.findCollapsedFold(hierarchy, - affectedStartOffset, affectedEndOffset) == null - ) { // no collapsed folds => no need to rebuild - rebuildViews = false; - } - } - - if (rebuildViews) { - /** - * Check the affected offsets against the current document boundaries - */ - int docLength = getDocument().getLength(); - int rebuildStartOffset = Math.min(affectedStartOffset, docLength); - int rebuildEndOffset = Math.min(affectedEndOffset, docLength); - offsetRebuild(rebuildStartOffset, rebuildEndOffset); - } - } finally { - updateLayout(); - layoutUnlock(); - } - } finally { - lockView.unlock(); - } - } - public @Override void paint(Graphics g, Shape allocation) { java.awt.Component c = getContainer(); if (c instanceof javax.swing.text.JTextComponent){ diff --git a/editor.lib/src/org/netbeans/modules/editor/lib/drawing/DrawEngineFakeDocView.java b/editor.lib/src/org/netbeans/modules/editor/lib/drawing/DrawEngineFakeDocView.java --- a/editor.lib/src/org/netbeans/modules/editor/lib/drawing/DrawEngineFakeDocView.java +++ b/editor.lib/src/org/netbeans/modules/editor/lib/drawing/DrawEngineFakeDocView.java @@ -45,7 +45,6 @@ package org.netbeans.modules.editor.lib.drawing; import javax.swing.text.Element; -import org.netbeans.api.editor.fold.Fold; /** * Fake view of the whole document supporting the code folding, operating from given startOffset @@ -83,11 +82,6 @@ } @Override - protected Fold nextCollapsedFold() { - return null; // simulate no collapsed folds - } - - @Override protected void attachListeners(){ } diff --git a/editor.lib/src/org/netbeans/modules/editor/lib/drawing/FoldMultiLineView.java b/editor.lib/src/org/netbeans/modules/editor/lib/drawing/FoldMultiLineView.java --- a/editor.lib/src/org/netbeans/modules/editor/lib/drawing/FoldMultiLineView.java +++ b/editor.lib/src/org/netbeans/modules/editor/lib/drawing/FoldMultiLineView.java @@ -46,14 +46,10 @@ import java.util.ArrayList; import java.util.List; -import javax.swing.text.BadLocationException; import javax.swing.text.Element; import javax.swing.text.JTextComponent; -import javax.swing.text.Position; import javax.swing.text.View; import javax.swing.text.ViewFactory; -import org.netbeans.api.editor.fold.Fold; -import org.netbeans.editor.BaseDocument; import org.netbeans.lib.editor.view.GapMultiLineView; /** @@ -109,40 +105,7 @@ // begining of the first line int lastViewEndOffset = lineElem.getStartOffset(); - int cnt = foldAndEndLineElemList.size(); List childViews = new ArrayList(); - for (int i = 0; i < cnt; i++) { - Fold fold = (Fold)foldAndEndLineElemList.get(i); - int foldStartOffset = fold.getStartOffset(); - int foldEndOffset = fold.getEndOffset(); - - BaseDocument doc = (BaseDocument) lineElem.getDocument(); - try { - if (foldStartOffset > lastViewEndOffset) { // need to insert intra-line fragment - View lineView = f.create(lineElem); // normal line view - View intraFrag = lineView.createFragment(lastViewEndOffset, foldStartOffset); - childViews.add(intraFrag); - lastViewEndOffset = foldStartOffset; - } - - // Create collapsed view - Position viewStartPos = doc.createPosition(foldStartOffset); - Position viewEndPos = doc.createPosition(foldEndOffset); - CollapsedView collapsedView = new CollapsedView(lineElem, - viewStartPos, viewEndPos, fold.getDescription()); - childViews.add(collapsedView); - lastViewEndOffset = foldEndOffset; - - } catch (BadLocationException e) { - throw new IllegalStateException("Failed to create view boundary positions"); // NOI18N - } - - // Fetch line element where the fold ends - i++; - lineElem = (Element)foldAndEndLineElemList.get(i); - lineElemEndOffset = lineElem.getEndOffset(); - } - // Append ending fragment if necessary // asserted non-empty list => foldEndOffset populated if (lastViewEndOffset < lineElemEndOffset) { // need ending fragment diff --git a/editor.mimelookup/manifest.mf b/editor.mimelookup/manifest.mf --- a/editor.mimelookup/manifest.mf +++ b/editor.mimelookup/manifest.mf @@ -1,7 +1,7 @@ Manifest-Version: 1.0 OpenIDE-Module: org.netbeans.modules.editor.mimelookup/1 OpenIDE-Module-Localizing-Bundle: org/netbeans/modules/editor/mimelookup/Bundle.properties -OpenIDE-Module-Specification-Version: 1.30 +OpenIDE-Module-Specification-Version: 1.31 OpenIDE-Module-Recommends: org.netbeans.spi.editor.mimelookup.MimeDataProvider AutoUpdate-Essential-Module: true diff --git a/editor.mimelookup/nbproject/org-netbeans-modules-editor-mimelookup.sig b/editor.mimelookup/nbproject/org-netbeans-modules-editor-mimelookup.sig --- a/editor.mimelookup/nbproject/org-netbeans-modules-editor-mimelookup.sig +++ b/editor.mimelookup/nbproject/org-netbeans-modules-editor-mimelookup.sig @@ -1,9 +1,10 @@ #Signature file v4.1 -#Version 1.26.1 +#Version 1.30 CLSS public abstract interface !annotation java.lang.Deprecated anno 0 java.lang.annotation.Documented() anno 0 java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy value=RUNTIME) + anno 0 java.lang.annotation.Target(java.lang.annotation.ElementType[] value=[CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE]) intf java.lang.annotation.Annotation CLSS public java.lang.Object @@ -124,6 +125,6 @@ meth public abstract <%0 extends java.lang.Object> {%%0} lookup(java.lang.Class<{%%0}>) meth public static org.openide.util.Lookup getDefault() supr java.lang.Object -hfds defaultLookup +hfds LOG,defaultLookup hcls DefLookup,Empty diff --git a/editor.mimelookup/src/org/netbeans/api/editor/mimelookup/MimePath.java b/editor.mimelookup/src/org/netbeans/api/editor/mimelookup/MimePath.java --- a/editor.mimelookup/src/org/netbeans/api/editor/mimelookup/MimePath.java +++ b/editor.mimelookup/src/org/netbeans/api/editor/mimelookup/MimePath.java @@ -532,6 +532,80 @@ return true; } + /** + * Returns the inherited Mime type. + * For {@link #EMPTY}, returns {@code null}. For most other mime types, returns + * {@code ""}. If the mime type derives from another one, such as text/ant+xml derives + * from xml, the return value will be the base mime type (text/xml in the example case). + *

+ * For MimePaths that identified embedded content (more components on the MimePath), + * the method returns the parent MIME of the last MIME type on the path + * + * @return inherited mime type, or {@code null}, if no parent exists (for {@link #EMPTY}) + */ + public String getInheritedType() { + if ("".equals(mimeType)) { + return null; + } + MimePath lastType = (size() == 1) ? this : MimePath.parse(mimeType); + List inheritedPaths = lastType.getInheritedPaths(null, null); + if (inheritedPaths.size() > 1) { + return inheritedPaths.get(1); + } else { + return null; + } + } + + + /** + * Returns the included Mime paths. + * For MimePath, which nests several MIME types (i.e. text/php/text/html/text/css), it enumerates + * sub-paths so that a following element represents one level of nesting of the content. + * For the example example, the return would be: + *

    + *
  1. text/php/text/html/text/css -- the full path + *
  2. text/html/text/css -- outer content is removed + *
  3. text/css -- the mime type of the identified content itself + *
  4. (empty string) + *
+ *

+ * If a MIME type on the path has a generic MIME type (i.e. text/x-ant+xml + * has a generic MIME type text/xml), that generic type will be inserted. For example, + * for text/java/text/x-ant+xml/text/javascript, the result will list: + *

    + *
  1. text/java/text/x-ant+xml/text/javascript, -- the full MimePath + *
  2. text/x-ant+xml/text/javascript -- a prefix + *
  3. text/xml/text/javascript -- ant+xml is generalized to xml + *
  4. text/javascript + *
  5. (empty string) + *
+ * For all but {@link #EMPTY} MimePaths, the list contains at least one entry, and the last + * entry is the {@link #EMPTY}. Note also, that the complete MimePath is always returned + * as the 1st member of the list. + *

+ * The returned sequence of MimePaths is suitable for searching settings or services + * for the (embedded) content whose type is described by MimePath as it is ordered from the + * most specific to the least specific paths (including generalization) and always contains + * the mime type of the identified contents. The last component ({@link ""}) represents + * default settings (services). + *

+ * Note that for MimePaths created from a mime type (not empty!) string, the + * getInheritedPaths().get(1) is a parent mime type. Either empty, + * or the generalized MIME. + *

+ * The caller should not modify the returned List. + * + * @return list of inherited Mime paths + */ + public List getIncludedPaths() { + List paths = getInheritedPaths(null, null); + List mpaths = new ArrayList(paths.size()); + for (String p : paths) { + mpaths.add(MimePath.parse(p)); + } + return mpaths; + } + // XXX: This is currently called from editor/settings/storage (SettingsProvider) // and editor/mimelookup/impl via reflection. // We will eventually make it friend API. In the meantime just diff --git a/editor.mimelookup/src/org/netbeans/modules/editor/mimelookup/MimePathLookup.java b/editor.mimelookup/src/org/netbeans/modules/editor/mimelookup/MimePathLookup.java --- a/editor.mimelookup/src/org/netbeans/modules/editor/mimelookup/MimePathLookup.java +++ b/editor.mimelookup/src/org/netbeans/modules/editor/mimelookup/MimePathLookup.java @@ -138,20 +138,8 @@ // See also http://www.netbeans.org/issues/show_bug.cgi?id=118099 // Add lookups from deprecated MimeLookupInitializers - List paths; - try { - Method m = MimePath.class.getDeclaredMethod("getInheritedPaths", String.class, String.class); //NOI18N - m.setAccessible(true); - @SuppressWarnings("unchecked") - List ret = (List) m.invoke(mimePath, null, null); - paths = ret; - } catch (Exception e) { - LOG.log(Level.WARNING, "Can't call org.netbeans.api.editor.mimelookup.MimePath.getInheritedPaths method.", e); //NOI18N - paths = Collections.singletonList(mimePath.getPath()); - } - - for(String path : paths) { - MimePath mp = MimePath.parse(path); + List paths = mimePath.getIncludedPaths(); + for(MimePath mp : paths) { Collection initializers = mimeInitializers.allInstances(); for(int i = 0; i < mp.size(); i++) { @@ -166,7 +154,7 @@ for(MimeLookupInitializer mli : initializers) { if (LOG.isLoggable(Level.FINE)) { - LOG.fine("- Querying MimeLookupInitializer(" + path + "): " + mli); //NOI18N + LOG.fine("- Querying MimeLookupInitializer(" + mp.getPath() + "): " + mli); //NOI18N } Lookup mimePathLookup = mli.lookup(); if (mimePathLookup != null) { diff --git a/editor.mimelookup/test/unit/src/org/netbeans/api/editor/mimelookup/MimePathTest.java b/editor.mimelookup/test/unit/src/org/netbeans/api/editor/mimelookup/MimePathTest.java --- a/editor.mimelookup/test/unit/src/org/netbeans/api/editor/mimelookup/MimePathTest.java +++ b/editor.mimelookup/test/unit/src/org/netbeans/api/editor/mimelookup/MimePathTest.java @@ -84,6 +84,8 @@ MimePath mpPrefix = mp.getPrefix(2); assertTrue("text/x-java/text/x-ant+xml".equals(mpPrefix.getPath())); + List ll = mp.getInheritedPaths(null, null); + // Force exceed size of the internal LRU cache and release the created and cached MPs for (int op = 0; op < 2; op++) { for (int i = 0; i < 10; i++) { diff --git a/editor.settings.storage/manifest.mf b/editor.settings.storage/manifest.mf --- a/editor.settings.storage/manifest.mf +++ b/editor.settings.storage/manifest.mf @@ -2,6 +2,6 @@ OpenIDE-Module: org.netbeans.modules.editor.settings.storage/1 OpenIDE-Module-Localizing-Bundle: org/netbeans/modules/editor/settings/storage/Bundle.properties OpenIDE-Module-Provides: org.netbeans.api.editor.settings.implementation -OpenIDE-Module-Specification-Version: 1.37 +OpenIDE-Module-Specification-Version: 1.38 OpenIDE-Module-Layer: org/netbeans/modules/editor/settings/storage/layer.xml AutoUpdate-Show-In-Client: false diff --git a/editor.settings.storage/nbproject/project.xml b/editor.settings.storage/nbproject/project.xml --- a/editor.settings.storage/nbproject/project.xml +++ b/editor.settings.storage/nbproject/project.xml @@ -55,7 +55,7 @@ 1 - 1.0 + 1.31 @@ -176,6 +176,7 @@ org.netbeans.modules.jvi org.netbeans.modules.languages org.netbeans.modules.options.editor + org.netbeans.modules.editor.fold org.netbeans.modules.parsing.api org.netbeans.modules.editor.settings.storage.api org.netbeans.modules.editor.settings.storage.spi diff --git a/editor.settings.storage/src/org/netbeans/modules/editor/settings/storage/api/EditorSettings.java b/editor.settings.storage/src/org/netbeans/modules/editor/settings/storage/api/EditorSettings.java --- a/editor.settings.storage/src/org/netbeans/modules/editor/settings/storage/api/EditorSettings.java +++ b/editor.settings.storage/src/org/netbeans/modules/editor/settings/storage/api/EditorSettings.java @@ -48,6 +48,7 @@ import java.util.Collection; import java.util.Map; import java.util.Set; +import java.util.prefs.Preferences; import javax.swing.text.AttributeSet; import org.netbeans.modules.editor.settings.storage.EditorSettingsImpl; import org.netbeans.modules.editor.settings.storage.MimeTypesTracker; @@ -321,4 +322,22 @@ String propertyName, PropertyChangeListener l ); + + /** + * Provides a temporary storage for Preferences, which can be + * flushed to the delegate. + * The returned object collects changes made through the {@link Preferences} + * interface, but does not write them to the 'delegate' instance immediately. + * It writes the changes (adds new keys, changes existing ones, removes the + * obsolete keys) during {@link Preferences#flush}. + *

+ * The token controls the lifecycle of the Preferences tree. If left unattended, + * it will be destroyed when the 'token' object becomes GCed. Each token instance + * has its own full Preferences tree. + * + * @param token token to control lifecycle and separate instances + * @param delegate the backing Preferences + * @return Preferences with temporary storage. + */ + // public abstract MemoryPreferences getProxyPreferences(Object token, Preferences delegate); } diff --git a/editor.settings.storage/src/org/netbeans/modules/editor/settings/storage/api/MemoryPreferences.java b/editor.settings.storage/src/org/netbeans/modules/editor/settings/storage/api/MemoryPreferences.java new file mode 100644 --- /dev/null +++ b/editor.settings.storage/src/org/netbeans/modules/editor/settings/storage/api/MemoryPreferences.java @@ -0,0 +1,171 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2013 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + * + * Contributor(s): + * + * Portions Copyrighted 2013 Sun Microsystems, Inc. + */ +package org.netbeans.modules.editor.settings.storage.api; + +import java.util.prefs.Preferences; +import org.netbeans.modules.editor.settings.storage.preferences.InheritedPreferences; +import org.netbeans.modules.editor.settings.storage.preferences.ProxyPreferencesImpl; + +/** + * Preferences with a temporary storage, backed by another Preferences + * object. The instance tracks modifications done through the + * {@link Preferences} interface, but do not change the backing store + * until {@link Preferences#flush} is called. + *

+ * The MemoryPreferences object serves as an accessor, and offers some + * additional control for the Preferences tree. It should not be handed + * away, only the creator who manages the lifecycle should possess + * the MemoryPreferences instance. Other clients should be given just the + * Preferences object from {@link #getPreferences}. + *

+ * The returned Preferences object implements {@link LocalPreferences} extension + * interface. + *

+ * This implementation does not support sub-nodes. + * + * @author sdedic + * @author Vita Stejskal + */ +public final class MemoryPreferences { + /** + * Returns an instance of Preferences backed by the delegate. + * A token is used to identify the desired Preferences set. As long as {@link #destroy} is not called, + * calls which use the same token & delegate will receive the same Preferences objects (though their + * MemoryPreferences may differ). The returned object implements {@link LocalPreferences} + * interface. + * + * @param token token that determines the tree of Preferences. + * @param delegate + * @return MemoryPreferences accessor instance + */ + public static MemoryPreferences get(Object token, Preferences delegate) { + return new MemoryPreferences(ProxyPreferencesImpl.getProxyPreferences(token, delegate)); + } + + /** + * Creates Preferences, which delegates to both persistent storage and parent (inherited) preferences. + * The persistent storage takes precedence over the parent. The {@link Preferences#remove} call is redefined + * for this case to just remove the key-value from the 'delegate', so that 'parent' value (if any) can become + * effective. Before {@link Preferences#flush}, the returned Preferences object delegates to both 'parent' + * and 'delegate' so that effective values can be seen. The returned object implements {@link LocalPreferences} + * interface. + * + * @param token + * @param parent + * @param delegate + * @return + */ + public static MemoryPreferences getWithInherited(Object token, Preferences parent, Preferences delegate) { + if (parent == null) { + return get(token, delegate); + } + InheritedPreferences inh = new InheritedPreferences(parent, delegate); + return new MemoryPreferences(ProxyPreferencesImpl.getProxyPreferences(token, inh)); + } + + /** + * Provides the Preferences instance. + * The instance will collect writes in memory, as described in {@link MemoryPreferences} doc. + * + * @return instance of Preferences + */ + public Preferences getPreferences() { + return prefInstance; + } + + /** + * Destroys the whole tree this Preferences belongs to. + * Individual Preferences node will not be flushed or cleared, but will become + * inaccessible from their token + * + * @see {@link EditorSettings#getProxyPreferences} + */ + public void destroy() { + prefInstance.destroy(); + } + + /** + * Suppresses events from this Preferences node. + * During the Runnable execution, the Preferences node will not + * fire any events. + * + * @param r runnable to execute + */ + public void runWithoutEvents(Runnable r) { + try { + prefInstance.silence(); + r.run(); + } finally { + prefInstance.noise(); + } + } + + /** + * Checks whether the Preferences node is changed. + * Only value provided by the {@link #getPreferences} and values derived by call to {@link Preferences#node} + * on that instance are supported. In other words, Preferences object from the tree managed by this + * MemoryPreferences object. IllegalArgumentException can be thrown when an incompatible Preferences object + * is used. + *

+ * True will be returned, if the Preferences object is dirty and not flushed. + * + * @param pref preferences node to check + * @return true, if the preferences was modified, and not flushed + * @throws IllegalArgumentException if the pref node is not from the managed tree. + */ + public boolean isDirty(Preferences pref) { + if (!(pref instanceof ProxyPreferencesImpl)) { + throw new IllegalArgumentException("Incompatible PreferencesImpl"); + } + ProxyPreferencesImpl impl = (ProxyPreferencesImpl)pref; + if (impl.node(prefInstance.absolutePath()) != prefInstance) { + throw new IllegalArgumentException("The preferences tree root is not reachable"); + } + return impl.isDirty(); + } + + private ProxyPreferencesImpl prefInstance; + + private MemoryPreferences(ProxyPreferencesImpl delegate) { + this.prefInstance = delegate; + } +} diff --git a/editor.settings.storage/src/org/netbeans/modules/editor/settings/storage/api/OverridePreferences.java b/editor.settings.storage/src/org/netbeans/modules/editor/settings/storage/api/OverridePreferences.java new file mode 100644 --- /dev/null +++ b/editor.settings.storage/src/org/netbeans/modules/editor/settings/storage/api/OverridePreferences.java @@ -0,0 +1,64 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2013 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + * + * Contributor(s): + * + * Portions Copyrighted 2013 Sun Microsystems, Inc. + */ +package org.netbeans.modules.editor.settings.storage.api; + +/** + * Mixin interface to detect if a value is inherited (defaulted) or not. + * The interface is to be implemented on Preferences objects (e.g. Mime Preferences), + * which support some sort of fallback, inheritance or default. It allows + * clients to determine whether a preference key is defined at the level represented + * by the Preferences object, or whether the value produced by {@link java.util.prefs.Preferences#get} + * originates in some form of default or inherited values. + * + * @author sdedic + */ +public interface OverridePreferences { + /** + * Determines whether the value is defined locally. + * If the value comes from an inherited or default set of values, + * the method returns {@code false}. + * + * @param key key to check + * @return true, if the value is defined locally, false if inherited. + */ + public boolean isOverriden(String key); +} diff --git a/editor.settings.storage/src/org/netbeans/modules/editor/settings/storage/fontscolors/CompositeFCS.java b/editor.settings.storage/src/org/netbeans/modules/editor/settings/storage/fontscolors/CompositeFCS.java --- a/editor.settings.storage/src/org/netbeans/modules/editor/settings/storage/fontscolors/CompositeFCS.java +++ b/editor.settings.storage/src/org/netbeans/modules/editor/settings/storage/fontscolors/CompositeFCS.java @@ -113,12 +113,12 @@ mimePath = mimePath.getPrefix(mimePath.size() - 1); } - MimePath[] allPaths = computeInheritedMimePaths(mimePath); - assert allPaths.length > 0 : "allPaths should always contain at least MimePath.EMPTY"; //NOI18N + List allPaths = mimePath.getIncludedPaths(); + assert allPaths.size() > 0 : "allPaths should always contain at least MimePath.EMPTY"; //NOI18N - this.allFcsi = new FontColorSettingsImpl[allPaths.length]; - for (int i = 0; i < allPaths.length; i++) { - allFcsi[i] = FontColorSettingsImpl.get(allPaths[i]); + this.allFcsi = new FontColorSettingsImpl[allPaths.size()]; + for (int i = 0; i < allPaths.size(); i++) { + allFcsi[i] = FontColorSettingsImpl.get(allPaths.get(i)); } this.profile = profile; @@ -275,31 +275,6 @@ System.out.println(sb.toString()); } - private static MimePath[] computeInheritedMimePaths(MimePath mimePath) { - List paths = null; - try { - Method m = MimePath.class.getDeclaredMethod("getInheritedPaths", String.class, String.class); //NOI18N - m.setAccessible(true); - @SuppressWarnings("unchecked") - List ret = (List) m.invoke(mimePath, null, null); - paths = ret; - } catch (Exception e) { - LOG.log(Level.WARNING, "Can't call org.netbeans.api.editor.mimelookup.MimePath.getInheritedPaths method.", e); //NOI18N - } - - if (paths != null) { - ArrayList mimePaths = new ArrayList(paths.size()); - - for (String path : paths) { - mimePaths.add(MimePath.parse(path)); - } - - return mimePaths.toArray(new MimePath[mimePaths.size()]); - } else { - return new MimePath[]{mimePath, MimePath.EMPTY}; - } - } - private Map getRenderingHints() { // This property was introduced in JDK1.6, see http://java.sun.com/javase/6/docs/api/java/awt/doc-files/DesktopProperties.html // We should probably also listen on the default toolkit for changes in this diff --git a/editor.settings.storage/src/org/netbeans/modules/editor/settings/storage/preferences/InheritedPreferences.java b/editor.settings.storage/src/org/netbeans/modules/editor/settings/storage/preferences/InheritedPreferences.java new file mode 100644 --- /dev/null +++ b/editor.settings.storage/src/org/netbeans/modules/editor/settings/storage/preferences/InheritedPreferences.java @@ -0,0 +1,258 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2013 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + * + * Contributor(s): + * + * Portions Copyrighted 2013 Sun Microsystems, Inc. + */ +package org.netbeans.modules.editor.settings.storage.preferences; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.prefs.AbstractPreferences; +import java.util.prefs.BackingStoreException; +import java.util.prefs.PreferenceChangeEvent; +import java.util.prefs.PreferenceChangeListener; +import java.util.prefs.Preferences; +import org.netbeans.modules.editor.settings.storage.api.OverridePreferences; + +/** + * Support for inheriting Preferences, while still working with stored ones. + * + * This class solves the 'diamond inheritance', which is present during editing: + * a MIME-type preferences derive from BOTH its persistent values (preferred) and + * from the parent, whose actual values are potentially transient, and also persistent. + *

+ * Let us assume the following assignment: + *

    + *
  • TC (this current) = currently added/changed/removed values + *
  • TP (this persistent) = persistent values, the getLocal() part of the Mime PreferencesImpl object + *
  • PC (parent current) = currently added/changed/removed values of the parent + *
  • PP (parent persistent) = persistent values, the getLocal() part of the parent MimePreferences + *
+ * The desired priority to find a value is: TC, TP, PC, PP. Because of {@link MemoryPreferences}, the + * PC, PP (and potentially fallback to a grandparent) we already have, if we use the parent's {@link MemoryPreferences} + * preferences as 'inherited'. The "TC" is handled by ProxyPreferences for this Mime node. In InheritedPreferences, + * we must only inject the TP in between TC and the parent's preferences (PC, PP, ...) + *

+ * The object is intended to act as a ProxyPreferences delegate, all writes go directly to the stored + * Mime preferences. + * + * @author sdedic + */ +public final class InheritedPreferences extends AbstractPreferences implements PreferenceChangeListener, OverridePreferences { + /** + * Preferences inherited, ie from a parent Mime type + */ + private Preferences inherited; + + /** + * Stored preferences, + */ + private Preferences stored; + + public InheritedPreferences(Preferences inherited, Preferences stored) { + super(null, ""); // NOI18N + this.inherited = inherited; + if (!(stored instanceof OverridePreferences)) { + throw new IllegalArgumentException(); + } + this.stored = stored; + + inherited.addPreferenceChangeListener(this); + } + + /* package */ Preferences getParent() { + return inherited; + } + + @Override + protected void putSpi(String key, String value) { + // do nothing, the AbstractPref then just fires an event + } + + @Override + public void put(String key, String value) { + if (Boolean.TRUE != ignorePut.get()) { + stored.put(key, value); + } + super.put(key, value); + } + + @Override + public void putInt(String key, int value) { + if (Boolean.TRUE != ignorePut.get()) { + stored.putInt(key, value); + } + super.putInt(key, value); + } + + @Override + public void putLong(String key, long value) { + if (Boolean.TRUE != ignorePut.get()) { + stored.putLong(key, value); + } + super.putLong(key, value); + } + + @Override + public void putBoolean(String key, boolean value) { + if (Boolean.TRUE != ignorePut.get()) { + stored.putBoolean(key, value); + } + super.putBoolean(key, value); + } + + @Override + public void putFloat(String key, float value) { + if (Boolean.TRUE != ignorePut.get()) { + stored.putFloat(key, value); + } + super.putFloat(key, value); + } + + @Override + public void putDouble(String key, double value) { + if (Boolean.TRUE != ignorePut.get()) { + stored.putDouble(key, value); + } + super.putDouble(key, value); + } + + @Override + public void putByteArray(String key, byte[] value) { + if (Boolean.TRUE != ignorePut.get()) { + stored.putByteArray(key, value); + } + super.putByteArray(key, value); + } + + private ThreadLocal ignorePut = new ThreadLocal(); + + @Override + public void preferenceChange(PreferenceChangeEvent evt) { + if (!isOverriden(evt.getKey())) { + // jusr refires an event + ignorePut.set(true); + try { + put(evt.getKey(), evt.getNewValue()); + } finally { + ignorePut.set(false); + } + } + } + + /** + * The value is defined locally, if the stored prefs define the value + * locally. The parent definitions do not count. It is expected, that the + * ProxyPreferences will report its local overrides as local in front of this + * InheritedPreferences. + * + * @param k + * @return + */ + public @Override boolean isOverriden(String k) { + if (stored instanceof OverridePreferences) { + return ((OverridePreferences)stored).isOverriden(k); + } else { + return true; + } + } + + @Override + protected String getSpi(String key) { + // check the stored values + OverridePreferences localStored = (OverridePreferences)stored; + if (localStored.isOverriden(key)) { + return stored.get(key, null); + } + // fall back to the inherited prefs, potentially its stored values etc. + return inherited.get(key, null); + } + + @Override + protected void removeSpi(String key) { + stored.remove(key); + } + + @Override + protected void removeNodeSpi() throws BackingStoreException { + stored.removeNode(); + } + + @Override + protected String[] keysSpi() throws BackingStoreException { + Collection names = new HashSet(); + names.addAll(Arrays.asList(stored.keys())); + names.addAll(Arrays.asList(inherited.keys())); + return names.toArray(new String[names.size()]); + } + + @Override + protected String[] childrenNamesSpi() throws BackingStoreException { + if (stored != null) { + return stored.childrenNames(); + } else { + return new String[0]; + } + } + + @Override + protected AbstractPreferences childSpi(String name) { + Preferences storedNode = stored != null ? stored.node(name) : null; + if (storedNode != null) { + return new InheritedPreferences(null, storedNode); + } else { + return null; + } + } + + @Override + protected void syncSpi() throws BackingStoreException { + stored.sync(); + } + + @Override + protected void flushSpi() throws BackingStoreException { + stored.flush(); + } + + +} diff --git a/editor.settings.storage/src/org/netbeans/modules/editor/settings/storage/preferences/PreferencesImpl.java b/editor.settings.storage/src/org/netbeans/modules/editor/settings/storage/preferences/PreferencesImpl.java --- a/editor.settings.storage/src/org/netbeans/modules/editor/settings/storage/preferences/PreferencesImpl.java +++ b/editor.settings.storage/src/org/netbeans/modules/editor/settings/storage/preferences/PreferencesImpl.java @@ -63,6 +63,7 @@ import java.util.prefs.Preferences; import org.netbeans.api.editor.mimelookup.MimePath; import org.netbeans.modules.editor.settings.storage.api.EditorSettingsStorage; +import org.netbeans.modules.editor.settings.storage.api.OverridePreferences; import org.netbeans.modules.editor.settings.storage.spi.TypedValue; import org.openide.util.RequestProcessor; import org.openide.util.WeakListeners; @@ -71,7 +72,7 @@ * * @author vita */ -public final class PreferencesImpl extends AbstractPreferences implements PreferenceChangeListener { +public final class PreferencesImpl extends AbstractPreferences implements PreferenceChangeListener, OverridePreferences { // the constant bellow is used in o.n.e.Settings!! private static final String JAVATYPE_KEY_PREFIX = "nbeditor-javaType-for-legacy-setting_"; //NOI18N @@ -154,6 +155,18 @@ } } } + + public @Override boolean isOverriden(String key) { + synchronized (lock) { + String bareKey; + if (key.startsWith(JAVATYPE_KEY_PREFIX)) { + bareKey = key.substring(JAVATYPE_KEY_PREFIX.length()); + } else { + bareKey = key; + } + return getLocal().containsKey(bareKey); + } + } public @Override void remove(String key) { synchronized(lock) { @@ -473,20 +486,9 @@ private Preferences getInherited() { if (inherited == null && mimePath.length() > 0) { - List paths = null; - try { - Method m = MimePath.class.getDeclaredMethod("getInheritedPaths", String.class, String.class); //NOI18N - m.setAccessible(true); - @SuppressWarnings("unchecked") - List ret = (List) m.invoke(MimePath.parse(mimePath), null, null); - paths = ret; - } catch (Exception e) { - LOG.log(Level.WARNING, "Can't call org.netbeans.api.editor.mimelookup.MimePath.getInheritedPaths method.", e); //NOI18N - } - - if (paths != null) { - assert paths.size() > 1 : "Wrong getInheritedPaths result size: " + paths.size(); //NOI18N - inherited = get(MimePath.parse(paths.get(1))); + String type = MimePath.parse(mimePath).getInheritedType(); + if (type != null) { + inherited = get(MimePath.parse(type)); } else { inherited = get(MimePath.EMPTY); } diff --git a/editor.settings.storage/src/org/netbeans/modules/editor/settings/storage/preferences/ProxyPreferencesImpl.java b/editor.settings.storage/src/org/netbeans/modules/editor/settings/storage/preferences/ProxyPreferencesImpl.java new file mode 100644 --- /dev/null +++ b/editor.settings.storage/src/org/netbeans/modules/editor/settings/storage/preferences/ProxyPreferencesImpl.java @@ -0,0 +1,1124 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2010 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + * + * Contributor(s): + * + * Portions Copyrighted 2008 Sun Microsystems, Inc. + */ + +package org.netbeans.modules.editor.settings.storage.preferences; + +import java.io.IOException; +import java.io.OutputStream; +import java.lang.ref.Reference; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.EventObject; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.WeakHashMap; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.prefs.BackingStoreException; +import java.util.prefs.NodeChangeEvent; +import java.util.prefs.NodeChangeListener; +import java.util.prefs.PreferenceChangeEvent; +import java.util.prefs.PreferenceChangeListener; +import java.util.prefs.Preferences; +import javax.xml.bind.DatatypeConverter; +import org.netbeans.modules.editor.settings.storage.api.OverridePreferences; +import org.netbeans.modules.editor.settings.storage.spi.TypedValue; +import org.openide.util.WeakListeners; + +/** + * Preferences impl that stores changes locally, and propagates them upon flush(). + * The implementation is an adapted (former) implementation from org.netbeans.modules.options.indentation.ProxyPreferences. + * The original was moved here, and adapted to work with 'diamond' double defaulting: 1st default is + * the persistent Preferences object, where the changes will be finally propagated. The 2nd default + * is the Preferences object for the MIMEtype parent (if it exists). Keys that do not exist + * in the stored Preferences should delegate to the MIME parent. During editing, the MIME parent Preferences + * may get also changed, so we cannot rely on delegation between stored Mime Preferences, but must + * inject an additional path - see {@link InheritedPreferences}. + * + * @author sdedic + * @author vita + */ +public final class ProxyPreferencesImpl extends Preferences implements PreferenceChangeListener, NodeChangeListener, + OverridePreferences { + + /** + * Inherited preferences, for the case that key does not exist at our Node. + * Special handling for diamond inheritance. + */ + private Preferences inheritedPrefs; + + public static ProxyPreferencesImpl getProxyPreferences(Object token, Preferences delegate) { + return Tree.getTree(token, delegate).get(null, delegate.name(), delegate); //NOI18N + } + + public boolean isDirty() { + synchronized (tree.treeLock()) { + return !(data.isEmpty() && removedKeys.isEmpty() && children.isEmpty() && removedChildren.isEmpty()) || removed; + } + } + + @Override + public void put(String key, String value) { + _put(key, value, String.class.getName()); + } + + @Override + public String get(String key, String def) { + synchronized (tree.treeLock()) { + checkNotNull(key, "key"); //NOI18N + checkRemoved(); + + if (removedKeys.contains(key)) { + if (LOG.isLoggable(Level.FINE)) { + LOG.fine("Key '" + key + "' removed, using default '" + def + "'"); //NOI18N + } + // removes will be flushed to the preferences, but now we need to see the defaults + // that WILL become effective after flush of this object. + if (inheritedPrefs != null) { + return inheritedPrefs.get(key, def); + } else { + return def; + } + } else { + TypedValue typedValue = data.get(key); + if (typedValue != null) { + if (LOG.isLoggable(Level.FINE)) { + LOG.fine("Key '" + key + "' modified, local value '" + typedValue.getValue() + "'"); //NOI18N + } + return typedValue.getValue(); + } else if (delegate != null) { + String value = delegate.get(key, def); + if (LOG.isLoggable(Level.FINE)) { + LOG.fine("Key '" + key + "' undefined, original value '" + value + "'"); //NOI18N + } + return value; + } else { + if (LOG.isLoggable(Level.FINE)) { + LOG.fine("Key '" + key + "' undefined, '" + name + "' is a new node, using default '" + def + "'"); //NOI18N + } + return def; + } + } + } + } + + @Override + public void remove(String key) { + EventBag bag = null; + + synchronized (tree.treeLock()) { + checkNotNull(key, "key"); //NOI18N + checkRemoved(); + + if (removedKeys.add(key)) { + data.remove(key); + bag = new EventBag(); + bag.addListeners(prefListeners); + if (inheritedPrefs != null) { + bag.addEvent(new PreferenceChangeEvent(this, key, + inheritedPrefs.get(key, null))); + } else { + bag.addEvent(new PreferenceChangeEvent(this, key, null)); + } + } + } + + if (bag != null) { + firePrefEvents(Collections.singletonList(bag)); + } + } + + @Override + public void clear() throws BackingStoreException { + EventBag bag = new EventBag(); + + synchronized (tree.treeLock()) { + checkRemoved(); + + // Determine modified or added keys + Set keys = new HashSet(); + keys.addAll(data.keySet()); + keys.removeAll(removedKeys); + if (!keys.isEmpty()) { + for(String key : keys) { + String value = delegate == null ? null : delegate.get(key, null); + bag.addEvent(new PreferenceChangeEvent(this, key, value)); + } + } + + // Determine removed keys + if (delegate != null) { + for(String key : removedKeys) { + String value = delegate.get(key, null); + if (value != null) { + bag.addEvent(new PreferenceChangeEvent(this, key, value)); + } + } + } + + // Initialize bag's listeners + bag.addListeners(prefListeners); + + // Finally, remove the data + data.clear(); + removedKeys.clear(); + } + + firePrefEvents(Collections.singletonList(bag)); + } + + @Override + public void putInt(String key, int value) { + _put(key, Integer.toString(value), Integer.class.getName()); + } + + @Override + public int getInt(String key, int def) { + String value = get(key, null); + if (value != null) { + try { + return Integer.parseInt(value); + } catch (NumberFormatException nfe) { + // ignore + } + } + return def; + } + + @Override + public void putLong(String key, long value) { + _put(key, Long.toString(value), Long.class.getName()); + } + + @Override + public long getLong(String key, long def) { + String value = get(key, null); + if (value != null) { + try { + return Long.parseLong(value); + } catch (NumberFormatException nfe) { + // ignore + } + } + return def; + } + + @Override + public void putBoolean(String key, boolean value) { + _put(key, Boolean.toString(value), Boolean.class.getName()); + } + + @Override + public boolean getBoolean(String key, boolean def) { + String value = get(key, null); + if (value != null) { + return Boolean.parseBoolean(value); + } else { + return def; + } + } + + @Override + public void putFloat(String key, float value) { + _put(key, Float.toString(value), Float.class.getName()); + } + + @Override + public float getFloat(String key, float def) { + String value = get(key, null); + if (value != null) { + try { + return Float.parseFloat(value); + } catch (NumberFormatException nfe) { + // ignore + } + } + return def; + } + + @Override + public void putDouble(String key, double value) { + _put(key, Double.toString(value), Double.class.getName()); + } + + @Override + public double getDouble(String key, double def) { + String value = get(key, null); + if (value != null) { + try { + return Double.parseDouble(value); + } catch (NumberFormatException nfe) { + // ignore + } + } + return def; + } + + @Override + public void putByteArray(String key, byte[] value) { + _put(key, DatatypeConverter.printBase64Binary(value), value.getClass().getName()); + } + + @Override + public byte[] getByteArray(String key, byte[] def) { + String value = get(key, null); + if (value != null) { + byte [] decoded = DatatypeConverter.parseBase64Binary(value); + if (decoded != null) { + return decoded; + } + } + return def; + } + + @Override + public String[] keys() throws BackingStoreException { + synchronized (tree.treeLock()) { + checkRemoved(); + HashSet keys = new HashSet(); + if (delegate != null) { + keys.addAll(Arrays.asList(delegate.keys())); + } + keys.addAll(data.keySet()); + keys.removeAll(removedKeys); + return keys.toArray(new String [keys.size()]); + } + } + + @Override + public String[] childrenNames() throws BackingStoreException { + synchronized (tree.treeLock()) { + checkRemoved(); + HashSet names = new HashSet(); + if (delegate != null) { + names.addAll(Arrays.asList(delegate.childrenNames())); + } + names.addAll(children.keySet()); + names.removeAll(removedChildren); + return names.toArray(new String [names.size()]); + } + } + + @Override + public Preferences parent() { + synchronized (tree.treeLock()) { + checkRemoved(); + return parent; + } + } + + @Override + public Preferences node(String pathName) { + Preferences node; + LinkedList> events = new LinkedList>(); + + synchronized (tree.treeLock()) { + checkNotNull(pathName, "pathName"); //NOI18N + checkRemoved(); + node = node(pathName, true, events); + } + + fireNodeEvents(events); + return node; + } + + @Override + public boolean nodeExists(String pathName) throws BackingStoreException { + synchronized (tree.treeLock()) { + if (pathName.length() == 0) { + return !removed; + } else { + checkRemoved(); + return node(pathName, false, null) != null; + } + } + } + + @Override + public void removeNode() throws BackingStoreException { + synchronized (tree.treeLock()) { + checkRemoved(); + ProxyPreferencesImpl p = parent; + if (p != null) { + p.removeChild(this); + } else { + throw new UnsupportedOperationException("Can't remove the root."); //NOI18N + } + } + } + + @Override + public String name() { + return name; + } + + @Override + public String absolutePath() { + synchronized (tree.treeLock()) { + ProxyPreferencesImpl pp = parent; + if (pp != null) { + if (pp.parent == null) { + // pp is the root, we don't want two consecutive slashes in the path + return "/" + name(); //NOI18N + } else { + return pp.absolutePath() + "/" + name(); //NOI18N + } + } else { + return "/"; //NOI18N + } + } + } + + @Override + public boolean isUserNode() { + synchronized (tree.treeLock()) { + if (delegate != null) { + return delegate.isUserNode(); + } else { + ProxyPreferencesImpl pp = parent; + if (pp != null) { + return pp.isUserNode(); + } else { + return true; + } + } + } + } + + @Override + public String toString() { + return (isUserNode() ? "User" : "System") + " Preference Node: " + absolutePath(); //NOI18N + } + + @Override + public void flush() throws BackingStoreException { + synchronized (tree.treeLock()) { + if (LOG.isLoggable(Level.FINE)) { + LOG.fine("Flushing " + absolutePath()); + } + + checkRemoved(); + for(ProxyPreferencesImpl pp : children.values()) { + pp.flush(); + } + + if (delegate == null) { + ProxyPreferencesImpl proxyRoot = parent.node("/", false, null); //NOI18N + assert proxyRoot != null : "Root must always exist"; //NOI18N + + Preferences delegateRoot = proxyRoot.delegate; + assert delegateRoot != null : "Root must always have its corresponding delegate"; //NOI18N + + Preferences nueDelegate = delegateRoot.node(absolutePath()); + changeDelegate(nueDelegate); + } + + delegate.removeNodeChangeListener(weakNodeListener); + delegate.removePreferenceChangeListener(weakPrefListener); + try { + // remove all removed children + for(String childName : removedChildren) { + if (delegate.nodeExists(childName)) { + delegate.node(childName).removeNode(); + } + } + + // write all valid key-value pairs + for(String key : data.keySet()) { + if (!removedKeys.contains(key)) { + if (LOG.isLoggable(Level.FINE)) { + LOG.fine("Writing " + absolutePath() + "/" + key + "=" + data.get(key)); + } + + TypedValue typedValue = data.get(key); + if (String.class.getName().equals(typedValue.getJavaType())) { + delegate.put(key, typedValue.getValue()); + + } else if (Integer.class.getName().equals(typedValue.getJavaType())) { + delegate.putInt(key, Integer.parseInt(typedValue.getValue())); + + } else if (Long.class.getName().equals(typedValue.getJavaType())) { + delegate.putLong(key, Long.parseLong(typedValue.getValue())); + + } else if (Boolean.class.getName().equals(typedValue.getJavaType())) { + delegate.putBoolean(key, Boolean.parseBoolean(typedValue.getValue())); + + } else if (Float.class.getName().equals(typedValue.getJavaType())) { + delegate.putFloat(key, Float.parseFloat(typedValue.getValue())); + + } else if (Double.class.getName().equals(typedValue.getJavaType())) { + delegate.putDouble(key, Double.parseDouble(typedValue.getValue())); + + } else { + delegate.putByteArray(key, DatatypeConverter.parseBase64Binary(typedValue.getValue())); + } + } + } + data.clear(); + + // remove all removed keys + for(String key : removedKeys) { + if (LOG.isLoggable(Level.FINE)) { + LOG.fine("Removing " + absolutePath() + "/" + key); + } + delegate.remove(key); + } + removedKeys.clear(); + } finally { + delegate.addNodeChangeListener(weakNodeListener); + delegate.addPreferenceChangeListener(weakPrefListener); + } + } + } + + @Override + public void sync() throws BackingStoreException { + ArrayList> prefEvents = new ArrayList>(); + ArrayList> nodeEvents = new ArrayList>(); + + synchronized (tree.treeLock()) { + _sync(prefEvents, nodeEvents); + } + + fireNodeEvents(nodeEvents); + firePrefEvents(prefEvents); + } + + @Override + public void addPreferenceChangeListener(PreferenceChangeListener pcl) { + synchronized (tree.treeLock()) { + prefListeners.add(pcl); + } + } + + @Override + public void removePreferenceChangeListener(PreferenceChangeListener pcl) { + synchronized (tree.treeLock()) { + prefListeners.remove(pcl); + } + } + + @Override + public void addNodeChangeListener(NodeChangeListener ncl) { + synchronized (tree.treeLock()) { + nodeListeners.add(ncl); + } + } + + @Override + public void removeNodeChangeListener(NodeChangeListener ncl) { + synchronized (tree.treeLock()) { + nodeListeners.remove(ncl); + } + } + + @Override + public void exportNode(OutputStream os) throws IOException, BackingStoreException { + throw new UnsupportedOperationException("exportNode not supported"); + } + + @Override + public void exportSubtree(OutputStream os) throws IOException, BackingStoreException { + throw new UnsupportedOperationException("exportSubtree not supported"); + } + + // ------------------------------------------------------------------------ + // PreferenceChangeListener implementation + // ------------------------------------------------------------------------ + + public void preferenceChange(PreferenceChangeEvent evt) { + PreferenceChangeListener [] listeners; + String nValue = evt.getNewValue(); + String k = evt.getKey(); + synchronized (tree.treeLock()) { + if (removed || data.containsKey(k)) { + return; + } + if (removedKeys.contains(k)) { + if (inheritedPrefs == null) { + return; + } else { + // if removed && there are inherited preferences, we must report the 'new value' + // from the inherited prefs, as the override in our preferences is not in effect now. + nValue = inheritedPrefs.get(k, null); + } + } + listeners = prefListeners.toArray(new PreferenceChangeListener[prefListeners.size()]); + } + + PreferenceChangeEvent myEvt = null; + for(PreferenceChangeListener l : listeners) { + if (myEvt == null) { + myEvt = new PreferenceChangeEvent(this, k, nValue); + } + l.preferenceChange(myEvt); + } + } + + // ------------------------------------------------------------------------ + // NodeChangeListener implementation + // ------------------------------------------------------------------------ + + public void childAdded(NodeChangeEvent evt) { + NodeChangeListener [] listeners; + Preferences childNode; + + synchronized (tree.treeLock()) { + String childName = evt.getChild().name(); + if (removed || removedChildren.contains(childName)) { + return; + } + + childNode = children.get(childName); + if (childNode != null) { + // swap delegates + ((ProxyPreferencesImpl) childNode).changeDelegate(evt.getChild()); + } else { + childNode = node(evt.getChild().name()); + } + + listeners = nodeListeners.toArray(new NodeChangeListener[nodeListeners.size()]); + } + + NodeChangeEvent myEvt = null; + for(NodeChangeListener l : listeners) { + if (myEvt == null) { + myEvt = new NodeChangeEvent(this, childNode); + } + l.childAdded(evt); + } + } + + public void childRemoved(NodeChangeEvent evt) { + NodeChangeListener [] listeners; + Preferences childNode; + + synchronized (tree.treeLock()) { + String childName = evt.getChild().name(); + if (removed || removedChildren.contains(childName)) { + return; + } + + childNode = children.get(childName); + if (childNode != null) { + // swap delegates + ((ProxyPreferencesImpl) childNode).changeDelegate(null); + } else { + // nobody has accessed the child yet + return; + } + + listeners = nodeListeners.toArray(new NodeChangeListener[nodeListeners.size()]); + } + + NodeChangeEvent myEvt = null; + for(NodeChangeListener l : listeners) { + if (myEvt == null) { + myEvt = new NodeChangeEvent(this, childNode); + } + l.childAdded(evt); + } + } + + // ------------------------------------------------------------------------ + // Other public implementation + // ------------------------------------------------------------------------ + + /** + * Destroys whole preferences tree as if called on the root. + */ + public void destroy() { + synchronized (tree.treeLock()) { + tree.destroy(); + } + } + + public void silence() { + synchronized (tree.treeLock()) { + noEvents = true; + } + } + + public void noise() { + synchronized (tree.treeLock()) { + noEvents = false; + } + } + + @Override + public boolean isOverriden(String key) { + return data.containsKey(key); + } + + // ------------------------------------------------------------------------ + // private implementation + // ------------------------------------------------------------------------ + + private static final Logger LOG = Logger.getLogger(ProxyPreferencesImpl.class.getName()); + + private final ProxyPreferencesImpl parent; + private final String name; + private Preferences delegate; + private final Tree tree; + private boolean removed; + + private final Map data = new HashMap(); + private final Set removedKeys = new HashSet(); + private final Map children = new HashMap(); + private final Set removedChildren = new HashSet(); + + private boolean noEvents = false; + private PreferenceChangeListener weakPrefListener; + private final Set prefListeners = new HashSet(); + private NodeChangeListener weakNodeListener; + private final Set nodeListeners = new HashSet(); + + private ProxyPreferencesImpl(ProxyPreferencesImpl parent, String name, Preferences delegate, Tree tree) { + assert name != null; + + this.parent = parent; + this.name = name; + this.delegate = delegate; + if (delegate instanceof InheritedPreferences) { + this.inheritedPrefs = ((InheritedPreferences)delegate).getParent(); + } + if (delegate != null) { + assert name.equals(delegate.name()); + + weakPrefListener = WeakListeners.create(PreferenceChangeListener.class, this, delegate); + delegate.addPreferenceChangeListener(weakPrefListener); + + weakNodeListener = WeakListeners.create(NodeChangeListener.class, this, delegate); + delegate.addNodeChangeListener(weakNodeListener); + } + this.tree = tree; + } + + private void _put(String key, String value, String javaType) { + EventBag bag = null; + + synchronized (tree.treeLock()) { + checkNotNull(key, "key"); //NOI18N + checkNotNull(value, "value"); //NOI18N + checkRemoved(); + + String orig = get(key, null); + if (orig == null || !orig.equals(value)) { + if (LOG.isLoggable(Level.FINE)) { + LOG.fine("Overwriting '" + key + "' = '" + value + "'"); //NOI18N + } + + data.put(key, new TypedValue(value, javaType)); + removedKeys.remove(key); + + bag = new EventBag(); + bag.addListeners(prefListeners); + bag.addEvent(new PreferenceChangeEvent(this, key, value)); + } + } + + if (bag != null) { + firePrefEvents(Collections.singletonList(bag)); + } + } + + private ProxyPreferencesImpl node(String pathName, boolean create, List> events) { + if (pathName.length() > 0 && pathName.charAt(0) == '/') { //NOI18N + // absolute path, if this is not the root then find the root + // and pass the call to it + if (parent != null) { + Preferences root = this; + while (root.parent() != null) { + root = root.parent(); + } + return ((ProxyPreferencesImpl) root).node(pathName, create, events); + } else { + // this is the root, change the pathName to a relative path and proceed + pathName = pathName.substring(1); + } + } + + if (pathName.length() > 0) { + String childName; + String pathFromChild; + + int idx = pathName.indexOf('/'); //NOI18N + if (idx != -1) { + childName = pathName.substring(0, idx); + pathFromChild = pathName.substring(idx + 1); + } else { + childName = pathName; + pathFromChild = null; + } + + ProxyPreferencesImpl child = children.get(childName); + if (child == null) { + if (removedChildren.contains(childName) && !create) { + // this child has been removed + return null; + } + + Preferences childDelegate = null; + try { + if (delegate != null && delegate.nodeExists(childName)) { + childDelegate = delegate.node(childName); + } + } catch (BackingStoreException bse) { + // ignore + } + + if (childDelegate != null || create) { + child = tree.get(this, childName, childDelegate); + children.put(childName, child); + removedChildren.remove(childName); + + // fire event if we really created the new child node + if (childDelegate == null) { + EventBag bag = new EventBag(); + bag.addListeners(nodeListeners); + bag.addEvent(new NodeChangeEventExt(this, child, false)); + events.add(bag); + } + } else { + // childDelegate == null && !create + return null; + } + } else { + assert !child.removed; + } + + return pathFromChild != null ? child.node(pathFromChild, create, events) : child; + } else { + return this; + } + } + + private void addChild(ProxyPreferencesImpl child) { + ProxyPreferencesImpl pp = children.get(child.name()); + if (pp == null) { + children.put(child.name(), child); + } else { + assert pp == child; + } + } + + private void removeChild(ProxyPreferencesImpl child) { + assert child != null; + assert children.get(child.name()) == child; + + child.nodeRemoved(); + children.remove(child.name()); + removedChildren.add(child.name()); + } + + private void nodeRemoved() { + for(ProxyPreferencesImpl pp : children.values()) { + pp.nodeRemoved(); + } + + data.clear(); + removedKeys.clear(); + children.clear(); + removedChildren.clear(); + tree.removeNode(this); + + removed = true; + } + + private void checkNotNull(Object paramValue, String paramName) { + if (paramValue == null) { + throw new NullPointerException("The " + paramName + " must not be null"); + } + } + + private void checkRemoved() { + if (removed) { + throw new IllegalStateException("The node '" + this + " has already been removed."); //NOI18N + } + } + + private void changeDelegate(Preferences nueDelegate) { + if (delegate != null) { + try { + if (delegate.nodeExists("")) { //NOI18N + assert weakPrefListener != null; + assert weakNodeListener != null; + delegate.removePreferenceChangeListener(weakPrefListener); + delegate.removeNodeChangeListener(weakNodeListener); + } + } catch (BackingStoreException bse) { + LOG.log(Level.WARNING, null, bse); + } + } + + delegate = nueDelegate; + weakPrefListener = null; + weakNodeListener = null; + + if (delegate != null) { + weakPrefListener = WeakListeners.create(PreferenceChangeListener.class, this, delegate); + delegate.addPreferenceChangeListener(weakPrefListener); + + weakNodeListener = WeakListeners.create(NodeChangeListener.class, this, delegate); + delegate.addNodeChangeListener(weakNodeListener); + } + } + + private void _sync( + List> prefEvents, + List> nodeEvents + ) { + // synchronize all children firts + for(ProxyPreferencesImpl pp : children.values()) { + pp._sync(prefEvents, nodeEvents); + } + + // report all new children as removed + EventBag nodeBag = new EventBag(); + nodeBag.addListeners(nodeListeners); + + for(ProxyPreferencesImpl pp : children.values()) { + if (pp.delegate == null) { + // new node that does not have corresponding node in the original hierarchy + nodeBag.addEvent(new NodeChangeEventExt(this, pp, true)); + } + } + + if (!nodeBag.getEvents().isEmpty()) { + nodeEvents.add(nodeBag); + } + + // report all modified keys + if (delegate != null) { + EventBag prefBag = new EventBag(); + prefBag.addListeners(prefListeners); + prefEvents.add(prefBag); + + for(String key : data.keySet()) { + prefBag.addEvent(new PreferenceChangeEvent(this, key, delegate.get(key, data.get(key).getValue()))); + } + } // else there is no corresponding node in the orig hierarchy and this node + // will be reported as removed + + // erase modified data + for(NodeChangeEvent nce : nodeBag.getEvents()) { + children.remove(nce.getChild().name()); + } + data.clear(); + } + + private void firePrefEvents(List> events) { + if (noEvents) { + return; + } + + for(EventBag bag : events) { + for(PreferenceChangeEvent event : bag.getEvents()) { + for(PreferenceChangeListener l : bag.getListeners()) { + try { + l.preferenceChange(event); + } catch (Throwable t) { + LOG.log(Level.WARNING, null, t); + } + } + } + } + } + + private void fireNodeEvents(List> events) { + if (noEvents) { + return; + } + + for(EventBag bag : events) { + for(NodeChangeEvent event : bag.getEvents()) { + for(NodeChangeListener l : bag.getListeners()) { + try { + if ((event instanceof NodeChangeEventExt) && ((NodeChangeEventExt) event).isRemovalEvent()) { + l.childRemoved(event); + } else { + l.childAdded(event); + } + } catch (Throwable t) { + LOG.log(Level.WARNING, null, t); + } + } + } + } + } + + /* test */ static final class Tree { + + public static Tree getTree(Object token, Preferences prefs) { + synchronized (trees) { + // find all trees for the token + Map forest = trees.get(token); + if (forest == null) { + forest = new HashMap(); + trees.put(token, forest); + } + + // find the tree for the prefs' root + Preferences root = prefs.node("/"); //NOI18N + Tree tree = forest.get(root); + if (tree == null) { + tree = new Tree(token, root); + forest.put(root, tree); + } + + return tree; + } + } + + /* test */ static final Map> trees = new WeakHashMap>(); + + private final Preferences root; + private final Reference tokenRef; + private final Map nodes = new HashMap(); + + private Tree(Object token, Preferences root) { + this.root = root; + this.tokenRef = new WeakReference(token); + } + + public Object treeLock() { + return this; + } + + public ProxyPreferencesImpl get(ProxyPreferencesImpl parent, String name, Preferences delegate) { + if (delegate != null) { + assert name.equals(delegate.name()); + + if (parent == null) { + Preferences parentDelegate = delegate.parent(); + if (parentDelegate != null) { + parent = get(null, parentDelegate.name(), parentDelegate); + } // else delegate is the root + } else { + // sanity check + assert parent.delegate == delegate.parent(); + } + } + + String absolutePath; + if (parent == null) { + absolutePath = "/"; //NOI18N + } else if (parent.parent() == null) { + absolutePath = "/" + name; //NOI18N + } else { + absolutePath = parent.absolutePath() + "/" + name; //NOI18N + } + + ProxyPreferencesImpl node = nodes.get(absolutePath); + if (node == null) { + node = new ProxyPreferencesImpl(parent, name, delegate, this); + nodes.put(absolutePath, node); + + if (parent != null) { + parent.addChild(node); + } + } else { + assert !node.removed; + } + + return node; + } + + public void removeNode(ProxyPreferencesImpl node) { + String path = node.absolutePath(); + assert nodes.containsKey(path); + ProxyPreferencesImpl pp = nodes.remove(path); + } + + public void destroy() { + synchronized (trees) { + Object token = tokenRef.get(); + if (token != null) { + trees.remove(token); + } // else the token has been GCed and therefore is not even in the trees map + } + } + } // End of Tree class + + private static final class EventBag { + private final Set listeners = new HashSet(); + private final Set events = new HashSet(); + + public EventBag() { + } + + public Set getListeners() { + return listeners; + } + + public Set getEvents() { + return events; + } + + public void addListeners(Collection l) { + listeners.addAll(l); + } + + public void addEvent(E event) { + events.add(event); + } + } // End of EventBag class + + private static final class NodeChangeEventExt extends NodeChangeEvent { + private final boolean removal; + public NodeChangeEventExt(Preferences parent, Preferences child, boolean removal) { + super(parent, child); + this.removal = removal; + } + + public boolean isRemovalEvent() { + return removal; + } + } // End of NodeChangeEventExt class +} diff --git a/editor.settings.storage/test/unit/src/org/netbeans/modules/editor/settings/storage/preferences/ProxyPreferencesImplTest.java b/editor.settings.storage/test/unit/src/org/netbeans/modules/editor/settings/storage/preferences/ProxyPreferencesImplTest.java new file mode 100644 --- /dev/null +++ b/editor.settings.storage/test/unit/src/org/netbeans/modules/editor/settings/storage/preferences/ProxyPreferencesImplTest.java @@ -0,0 +1,544 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2010 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + * + * Contributor(s): + * + * Portions Copyrighted 2008 Sun Microsystems, Inc. + */ + +package org.netbeans.modules.editor.settings.storage.preferences; + +import java.lang.ref.Reference; +import java.lang.ref.WeakReference; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.prefs.AbstractPreferences; +import java.util.prefs.BackingStoreException; +import java.util.prefs.Preferences; +import static junit.framework.Assert.assertEquals; +import org.netbeans.junit.NbTestCase; +import org.netbeans.modules.editor.settings.storage.api.OverridePreferences; +import org.netbeans.modules.editor.settings.storage.api.MemoryPreferences; + +/** + * + * @author vita + */ +public class ProxyPreferencesImplTest extends NbTestCase { + + public ProxyPreferencesImplTest(String name) { + super(name); + } + + public void testSimpleRead() { + Preferences orig = Preferences.userRoot().node(getName()); + orig.put("key-1", "value-1"); + + Preferences test = ProxyPreferencesImpl.getProxyPreferences(this, orig); + assertEquals("Wrong value", "value-1", test.get("key-1", null)); + } + + public void testSimpleWrite() { + Preferences orig = Preferences.userRoot().node(getName()); + assertNull("Original contains value", orig.get("key-1", null)); + + Preferences test = ProxyPreferencesImpl.getProxyPreferences(this, orig); + test.put("key-1", "xyz"); + assertEquals("Wrong value", "xyz", test.get("key-1", null)); + } + + public void testBase64() { + Preferences orig = Preferences.userRoot().node(getName()); + assertNull("Original contains value", orig.get("key-1", null)); + Preferences test = ProxyPreferencesImpl.getProxyPreferences(this, orig); + test.putByteArray("key-1", "however you like it".getBytes()); + assertEquals("Wrong value", "however you like it", new String(test.getByteArray("key-1", null))); + } + + public void testSimpleSync() throws BackingStoreException { + Preferences orig = Preferences.userRoot().node(getName()); + assertNull("Original contains value", orig.get("key-1", null)); + + Preferences test = ProxyPreferencesImpl.getProxyPreferences(this, orig); + assertNull("Test should not contains pair", orig.get("key-1", null)); + + test.put("key-1", "xyz"); + assertEquals("Test doesn't contain new pair", "xyz", test.get("key-1", null)); + + test.sync(); + assertNull("Test didn't rollback pair", test.get("key-1", null)); + } + + public void testSimpleFlush() throws BackingStoreException { + Preferences orig = Preferences.userRoot().node(getName()); + assertNull("Original contains value", orig.get("key-1", null)); + + Preferences test = ProxyPreferencesImpl.getProxyPreferences(this, orig); + assertNull("Test should not contains pair", orig.get("key-1", null)); + + test.put("key-1", "xyz"); + assertEquals("Test doesn't contain new pair", "xyz", test.get("key-1", null)); + + test.flush(); + assertEquals("Test should still contain the pair", "xyz", test.get("key-1", null)); + assertEquals("Test didn't flush the pair", "xyz", orig.get("key-1", null)); + } + + public void testSyncTree1() throws BackingStoreException { + String [] origTree = new String [] { + "CodeStyle/profile=GLOBAL", + }; + String [] newTree = new String [] { + "CodeStyle/text/x-java/tab-size=2", + "CodeStyle/text/x-java/override-global-settings=true", + "CodeStyle/text/x-java/expand-tabs=true", + "CodeStyle/profile=PROJECT", + }; + + Preferences orig = Preferences.userRoot().node(getName()); + write(orig, origTree); + checkContains(orig, origTree, "Orig"); + checkNotContains(orig, newTree, "Orig"); + + Preferences test = ProxyPreferencesImpl.getProxyPreferences(this, orig); + checkEquals("Test should be the same as Orig", orig, test); + + write(test, newTree); + checkContains(test, newTree, "Test"); + + test.sync(); + checkContains(orig, origTree, "Orig"); + checkNotContains(orig, newTree, "Orig"); + checkContains(test, origTree, "Test"); + checkNotContains(test, newTree, "Test"); + } + + public void testFlushTree1() throws BackingStoreException { + String [] origTree = new String [] { + "CodeStyle/profile=GLOBAL", + }; + String [] newTree = new String [] { + "CodeStyle/text/x-java/tab-size=2", + "CodeStyle/text/x-java/override-global-settings=true", + "CodeStyle/text/x-java/expand-tabs=true", + "CodeStyle/profile=PROJECT", + }; + + Preferences orig = Preferences.userRoot().node(getName()); + write(orig, origTree); + checkContains(orig, origTree, "Orig"); + checkNotContains(orig, newTree, "Orig"); + + Preferences test = ProxyPreferencesImpl.getProxyPreferences(this, orig); + checkEquals("Test should be the same as Orig", orig, test); + + write(test, newTree); + checkContains(test, newTree, "Test"); + + test.flush(); + checkEquals("Test didn't flush to Orig", test, orig); + } + + public void testRemoveKey() throws BackingStoreException { + Preferences orig = Preferences.userRoot().node(getName()); + orig.put("key-2", "value-2"); + assertNull("Original contains value", orig.get("key-1", null)); + assertEquals("Original doesn't contain value", "value-2", orig.get("key-2", null)); + + Preferences test = ProxyPreferencesImpl.getProxyPreferences(this, orig); + test.put("key-1", "xyz"); + assertEquals("Wrong value", "xyz", test.get("key-1", null)); + + test.remove("key-1"); + assertNull("Test contains removed key-1", test.get("key-1", null)); + + test.remove("key-2"); + assertNull("Test contains removed key-2", test.get("key-2", null)); + + test.flush(); + assertNull("Test flushed removed key-1", orig.get("key-1", null)); + assertNull("Test.flush did not remove removed key-2", orig.get("key-2", null)); + } + + public void testRemoveNode() throws BackingStoreException { + Preferences orig = Preferences.userRoot().node(getName()); + Preferences origChild = orig.node("child"); + + Preferences test = ProxyPreferencesImpl.getProxyPreferences(this, orig); + assertTrue("Test child shoculd exist", test.nodeExists("child")); + Preferences testChild = test.node("child"); + + testChild.removeNode(); + assertFalse("Removed test child should not exist", testChild.nodeExists("")); + assertFalse("Removed test child should not exist in parent", test.nodeExists("child")); + + test.flush(); + assertFalse("Test.flush did not remove orig child", origChild.nodeExists("")); + assertFalse("Test.flush did not remove orig child from parent", orig.nodeExists("child")); + } + + public void testRemoveNodeCreateItAgain() throws BackingStoreException { + Preferences orig = Preferences.userRoot().node(getName()); + + Preferences test = ProxyPreferencesImpl.getProxyPreferences(this, orig); + Preferences testChild = test.node("child"); + + testChild.removeNode(); + assertFalse("Removed test child should not exist", testChild.nodeExists("")); + assertFalse("Removed test child should not exist in parent", test.nodeExists("child")); + + Preferences testChild2 = test.node("child"); + assertTrue("Recreated test child should exist", testChild2.nodeExists("")); + assertTrue("Recreated test child should exist in parent", test.nodeExists("child")); + assertNotSame("Recreated child must not be the same as the removed one", testChild2, testChild); + assertEquals("Wrong childrenNames list", Arrays.asList(new String [] { "child" }), Arrays.asList(test.childrenNames())); + + try { + testChild.get("key", null); + fail("Removed test node should not be accessible"); + } catch (Exception e) { + } + + try { + testChild2.get("key", null); + } catch (Exception e) { + fail("Recreated test node should be accessible"); + } + + } + + public void testRemoveHierarchy() throws BackingStoreException { + String [] origTree = new String [] { + "R.CodeStyle.project.expand-tabs=true", + "R.CodeStyle.project.indent-shift-width=6", + "R.CodeStyle.project.spaces-per-tab=6", + "R.CodeStyle.project.tab-size=7", + "R.CodeStyle.project.text-limit-width=88", + "R.CodeStyle.usedProfile=project", + "R.text.x-ruby.CodeStyle.project.indent-shift-width=2", + "R.text.x-ruby.CodeStyle.project.spaces-per-tab=2", + "R.text.x-ruby.CodeStyle.project.tab-size=2", + }; + String [] newTree = new String [] { + "R.CodeStyle.project.expand-tabs=true", + "R.CodeStyle.project.indent-shift-width=3", + "R.CodeStyle.project.spaces-per-tab=3", + "R.CodeStyle.project.tab-size=5", + "R.CodeStyle.project.text-limit-width=77", + "R.CodeStyle.usedProfile=project", + "R.text.x-ruby.CodeStyle.project.indent-shift-width=2", + "R.text.x-ruby.CodeStyle.project.spaces-per-tab=2", + "R.text.x-ruby.CodeStyle.project.tab-size=2", + }; + + Preferences orig = Preferences.userRoot().node(getName()); + write(orig, origTree); + + checkContains(orig, origTree, "Orig"); + + Preferences test = ProxyPreferencesImpl.getProxyPreferences(this, orig); + checkEquals("Test should be the same as Orig", orig, test); + + Preferences testRoot = test.node("R"); + removeAllKidsAndKeys(testRoot); + + write(test, newTree); + checkContains(test, newTree, "Test"); + + test.flush(); + checkEquals("Test didn't flush to Orig", test, orig); + } + + public void testTreeGCed() throws BackingStoreException { + String [] newTree = new String [] { + "R.CodeStyle.project.expand-tabs=true", + "R.CodeStyle.project.indent-shift-width=3", + "R.CodeStyle.project.spaces-per-tab=3", + "R.CodeStyle.project.tab-size=5", + "R.CodeStyle.project.text-limit-width=77", + "R.CodeStyle.usedProfile=project", + "R.text.x-ruby.CodeStyle.project.indent-shift-width=2", + "R.text.x-ruby.CodeStyle.project.spaces-per-tab=2", + "R.text.x-ruby.CodeStyle.project.tab-size=2", + }; + + Preferences orig = Preferences.userRoot().node(getName()); + + Object treeToken = new Object(); + Preferences test = ProxyPreferencesImpl.getProxyPreferences(treeToken, orig); + write(test, newTree); + checkContains(test, newTree, "Test"); + + Reference treeTokenRef = new WeakReference(treeToken); + Reference testRef = new WeakReference(test); + treeToken = null; + test = null; + assertGC("Tree token was not GCed", treeTokenRef, Collections.singleton(this)); + // touch the WeakHashMap to expungeStaleEntries + Object dummyToken = new Object(); + ProxyPreferencesImpl dummyPrefs = ProxyPreferencesImpl.getProxyPreferences(dummyToken, orig); + assertGC("Test preferences were not GCed", testRef, Collections.singleton(this)); + + } + + /** + * Checks that a value not defined in delegate can be read from the parent prefs. + * Checks that if the parent prefs also do not define the value, the + * default from parameter is used. + * + * @throws Exception + */ + public void testInheritedRead() throws Exception { + Preferences stored = new MapPreferences(); + Preferences inherited = new MapPreferences(); + + stored.put("key-1", "value-1"); + stored.put("key-3", "override"); + inherited.put("key-2", "value-2"); + inherited.put("key-3", "base"); + + MemoryPreferences mem = MemoryPreferences.getWithInherited(this, inherited, stored); + Preferences test = mem.getPreferences(); + + assertEquals("Wrong value 1", "value-1", test.get("key-1", null)); + assertEquals("Wrong value 2", "value-2", test.get("key-2", "a")); + assertEquals("Wrong value 3", "override", test.get("key-3", "a")); + assertEquals("Wrong value 4", "value-4", test.get("key-4", "value-4")); + } + + /** + * Asserts that if a value is remove()d during editing, the inherited value + * will be seen through. Also checks that the Preferences key is actually + * deleted on flush() and the inherited preferences is not altered. + */ + public void testSeeInheritedThroughRemoves() throws Exception { + Preferences stored = new MapPreferences(); + Preferences inherited = new MapPreferences(); + + stored.put("key", "value"); + inherited.put("key", "parentValue"); + + MemoryPreferences mem = MemoryPreferences.getWithInherited(this, inherited, stored); + Preferences test = mem.getPreferences(); + + assertEquals("Does not see local value", "value", test.get("key", null)); + test.remove("key"); + + assertEquals("Stored value changed prematurely", "value", stored.get("key", null)); + assertEquals("Inherited not seen", "parentValue", test.get("key", null)); + + test.flush(); + assertNull("Stored value not erased", stored.get("key", null)); + assertEquals("Inherited changed", "parentValue", test.get("key", null)); + } + + // ----------------------------------------------------------------------- + // private implementation + // ----------------------------------------------------------------------- + + private static class MapPreferences extends AbstractPreferences implements OverridePreferences { + + private Map map = new HashMap(); + + public MapPreferences() { + super(null, ""); // NOI18N + } + + @Override + public boolean isOverriden(String key) { + return map.containsKey(key); + } + + protected void putSpi(String key, String value) { + map.put(key, value); + } + + protected String getSpi(String key) { + return (String)map.get(key); + } + + protected void removeSpi(String key) { + map.remove(key); + } + + protected void removeNodeSpi() throws BackingStoreException { + throw new UnsupportedOperationException("Not supported yet."); + } + + protected String[] keysSpi() throws BackingStoreException { + String array[] = new String[map.keySet().size()]; + return map.keySet().toArray( array ); + } + + protected String[] childrenNamesSpi() throws BackingStoreException { + throw new UnsupportedOperationException("Not supported yet."); + } + + protected AbstractPreferences childSpi(String name) { + throw new UnsupportedOperationException("Not supported yet."); + } + + protected void syncSpi() throws BackingStoreException { + throw new UnsupportedOperationException("Not supported yet."); + } + + protected void flushSpi() throws BackingStoreException { + throw new UnsupportedOperationException("Not supported yet."); + } + } + + private void write(Preferences prefs, String[] tree) { + for(String s : tree) { + int equalIdx = s.lastIndexOf('='); + assertTrue(equalIdx != -1); + String value = s.substring(equalIdx + 1); + + String key; + String nodePath; + int slashIdx = s.lastIndexOf('/', equalIdx); + if (slashIdx != -1) { + key = s.substring(slashIdx + 1, equalIdx); + nodePath = s.substring(0, slashIdx); + } else { + key = s.substring(0, equalIdx); + nodePath = ""; + } + + Preferences node = prefs.node(nodePath); + node.put(key, value); + } + } + + private void checkContains(Preferences prefs, String[] tree, String prefsId) throws BackingStoreException { + for(String s : tree) { + int equalIdx = s.lastIndexOf('='); + assertTrue(equalIdx != -1); + String value = s.substring(equalIdx + 1); + + String key; + String nodePath; + int slashIdx = s.lastIndexOf('/', equalIdx); + if (slashIdx != -1) { + key = s.substring(slashIdx + 1, equalIdx); + nodePath = s.substring(0, slashIdx); + } else { + key = s.substring(0, equalIdx); + nodePath = ""; + } + + assertTrue(prefsId + " doesn't contain node '" + nodePath + "'", prefs.nodeExists(nodePath)); + Preferences node = prefs.node(nodePath); + + String realValue = node.get(key, null); + assertNotNull(prefsId + ", '" + nodePath + "' node doesn't contain key '" + key + "'", realValue); + assertEquals(prefsId + ", '" + nodePath + "' node, '" + key + "' contains wrong value", value, realValue); + } + } + + private void checkNotContains(Preferences prefs, String[] tree, String prefsId) throws BackingStoreException { + for(String s : tree) { + int equalIdx = s.lastIndexOf('='); + assertTrue(equalIdx != -1); + String value = s.substring(equalIdx + 1); + + String key; + String nodePath; + int slashIdx = s.lastIndexOf('/', equalIdx); + if (slashIdx != -1) { + key = s.substring(slashIdx + 1, equalIdx); + nodePath = s.substring(0, slashIdx); + } else { + key = s.substring(0, equalIdx); + nodePath = ""; + } + + if (prefs.nodeExists(nodePath)) { + Preferences node = prefs.node(nodePath); + String realValue = node.get(key, null); + if (realValue != null && realValue.equals(value)) { + fail(prefsId + ", '" + nodePath + "' node contains key '" + key + "' = '" + realValue + "'"); + } + } + } + } + + private void dump(Preferences prefs, String prefsId) throws BackingStoreException { + for(String key : prefs.keys()) { + System.out.println(prefsId + ", " + prefs.absolutePath() + "/" + key + "=" + prefs.get(key, null)); + } + for(String child : prefs.childrenNames()) { + dump(prefs.node(child), prefsId); + } + } + + private void checkEquals(String msg, Preferences expected, Preferences test) throws BackingStoreException { + assertEquals("Won't compare two Preferences with different absolutePath", expected.absolutePath(), test.absolutePath()); + + // check the keys and their values + for(String key : expected.keys()) { + String expectedValue = expected.get(key, null); + assertNotNull(msg + "; Expected:" + expected.absolutePath() + " has no '" + key + "'", expectedValue); + + String value = test.get(key, null); + assertNotNull(msg + "; Test:" + test.absolutePath() + " has no '" + key + "'", value); + assertEquals(msg + "; Test:" + test.absolutePath() + "/" + key + " has wrong value", expectedValue, value); + } + + // check the children + for(String child : expected.childrenNames()) { + assertTrue(msg + "; Expected:" + expected.absolutePath() + " has no '" + child + "' subnode", expected.nodeExists(child)); + Preferences expectedChild = expected.node(child); + + assertTrue(msg + "; Test:" + test.absolutePath() + " has no '" + child + "' subnode", test.nodeExists(child)); + Preferences testChild = test.node(child); + + checkEquals(msg, expectedChild, testChild); + } + } + + private void removeAllKidsAndKeys(Preferences prefs) throws BackingStoreException { + for(String kid : prefs.childrenNames()) { + prefs.node(kid).removeNode(); + } + for(String key : prefs.keys()) { + prefs.remove(key); + } + } + +} diff --git a/editor/src/org/netbeans/modules/editor/resources/layer.xml b/editor/src/org/netbeans/modules/editor/resources/layer.xml --- a/editor/src/org/netbeans/modules/editor/resources/layer.xml +++ b/editor/src/org/netbeans/modules/editor/resources/layer.xml @@ -251,6 +251,13 @@ + + + + + + + diff --git a/html.editor/nbproject/project.xml b/html.editor/nbproject/project.xml --- a/html.editor/nbproject/project.xml +++ b/html.editor/nbproject/project.xml @@ -123,6 +123,10 @@ org.netbeans.modules.editor.fold + + 1 + 1.34 + org.netbeans.modules.editor.indent diff --git a/html.editor/src/org/netbeans/modules/html/editor/gsf/HtmlFoldTypeProvider.java b/html.editor/src/org/netbeans/modules/html/editor/gsf/HtmlFoldTypeProvider.java new file mode 100644 --- /dev/null +++ b/html.editor/src/org/netbeans/modules/html/editor/gsf/HtmlFoldTypeProvider.java @@ -0,0 +1,74 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2013 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + * + * Contributor(s): + * + * Portions Copyrighted 2013 Sun Microsystems, Inc. + */ +package org.netbeans.modules.html.editor.gsf; + +import java.util.Arrays; +import java.util.Collection; +import org.netbeans.api.editor.fold.FoldType; +import org.netbeans.api.editor.mimelookup.MimeRegistration; +import org.netbeans.api.editor.mimelookup.MimeRegistrations; +import org.netbeans.spi.editor.fold.FoldTypeProvider; + +/** + * @author sdedic + */ +@MimeRegistrations({ + @MimeRegistration(mimeType = "text/html", service = FoldTypeProvider.class), + @MimeRegistration(mimeType = "text/xhtml", service = FoldTypeProvider.class) +}) +public class HtmlFoldTypeProvider implements FoldTypeProvider { + private Collection TYPES = Arrays.asList(new FoldType[] { + HtmlStructureScanner.TYPE_TAG, + HtmlStructureScanner.TYPE_COMMENT + }); + + @Override + public Collection getValues(Class type) { + return type == FoldType.class ? TYPES : null; + } + + @Override + public boolean inheritable() { + return false; + } + +} diff --git a/html.editor/src/org/netbeans/modules/html/editor/gsf/HtmlStructureScanner.java b/html.editor/src/org/netbeans/modules/html/editor/gsf/HtmlStructureScanner.java --- a/html.editor/src/org/netbeans/modules/html/editor/gsf/HtmlStructureScanner.java +++ b/html.editor/src/org/netbeans/modules/html/editor/gsf/HtmlStructureScanner.java @@ -47,6 +47,7 @@ import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.text.BadLocationException; +import org.netbeans.api.editor.fold.FoldType; import org.netbeans.editor.BaseDocument; import org.netbeans.editor.Utilities; import org.netbeans.modules.csl.api.OffsetRange; @@ -58,12 +59,27 @@ import org.netbeans.modules.parsing.api.Snapshot; import org.netbeans.modules.web.common.api.Pair; import org.openide.filesystems.FileObject; +import org.openide.util.Exceptions; +import org.openide.util.NbBundle; +import static org.netbeans.modules.html.editor.gsf.Bundle.*; /** * * @author mfukala@netbeans.org */ public class HtmlStructureScanner implements StructureScanner { + + /** + * Tag fold type. Overrides the default label. + */ + @NbBundle.Messages("FT_Tag=Tags") + public static final FoldType TYPE_TAG = FoldType.TAG.override( + FT_Tag(), FoldType.TAG.getTemplate()); + + /** + * HTML comments + */ + public static final FoldType TYPE_COMMENT = FoldType.COMMENT; private static final Logger LOGGER = Logger.getLogger(HtmlStructureScanner.class.getName()); private static final boolean LOG = LOGGER.isLoggable(Level.FINE); @@ -187,8 +203,8 @@ } finally { doc.readUnlock(); } - folds.put("tags", tags); - folds.put("comments", comments); + folds.put(TYPE_TAG.code(), tags); + folds.put(TYPE_COMMENT.code(), comments); return folds; } diff --git a/html.editor/src/org/netbeans/modules/html/editor/resources/layer.xml b/html.editor/src/org/netbeans/modules/html/editor/resources/layer.xml --- a/html.editor/src/org/netbeans/modules/html/editor/resources/layer.xml +++ b/html.editor/src/org/netbeans/modules/html/editor/resources/layer.xml @@ -133,6 +133,14 @@ + + + + + + + + @@ -268,6 +276,14 @@ + + + + + + + + diff --git a/java.editor.lib/manifest.mf b/java.editor.lib/manifest.mf --- a/java.editor.lib/manifest.mf +++ b/java.editor.lib/manifest.mf @@ -1,5 +1,5 @@ Manifest-Version: 1.0 OpenIDE-Module: org.netbeans.modules.java.editor.lib/1 OpenIDE-Module-Localizing-Bundle: org/netbeans/lib/java/editor/Bundle.properties -OpenIDE-Module-Specification-Version: 1.28 +OpenIDE-Module-Specification-Version: 1.29 AutoUpdate-Show-In-Client: false diff --git a/java.editor.lib/nbproject/project.properties b/java.editor.lib/nbproject/project.properties --- a/java.editor.lib/nbproject/project.properties +++ b/java.editor.lib/nbproject/project.properties @@ -40,6 +40,8 @@ # Version 2 license, then the option applies only if the new code is # made subject to such option by the copyright holder. +javac.compilerargs=-Xlint -Xlint:-serial +javac.source=1.5 #javadoc.arch=${basedir}/arch/arch-java-editor-lib.xml javadoc.title=Java Editor Library javadoc.apichanges=${basedir}/apichanges.xml diff --git a/java.editor.lib/nbproject/project.xml b/java.editor.lib/nbproject/project.xml --- a/java.editor.lib/nbproject/project.xml +++ b/java.editor.lib/nbproject/project.xml @@ -50,11 +50,21 @@ org.netbeans.modules.java.editor.lib + org.netbeans.modules.editor.deprecated.pre65formatting + + + + 0-1 + 1.0 + + + org.netbeans.modules.editor.fold 1 + 1.34 @@ -76,6 +86,15 @@ + org.netbeans.modules.editor.lib2 + + + + 1 + 1.71 + + + org.netbeans.modules.editor.mimelookup @@ -108,15 +127,6 @@ 8.0 - - org.netbeans.modules.editor.deprecated.pre65formatting - - - - 0-1 - 1.0 - - org.netbeans.editor.ext.java diff --git a/java.editor.lib/src/org/netbeans/editor/ext/java/JavaFoldManager.java b/java.editor.lib/src/org/netbeans/editor/ext/java/JavaFoldManager.java --- a/java.editor.lib/src/org/netbeans/editor/ext/java/JavaFoldManager.java +++ b/java.editor.lib/src/org/netbeans/editor/ext/java/JavaFoldManager.java @@ -44,11 +44,12 @@ package org.netbeans.editor.ext.java; -import org.netbeans.api.editor.fold.Fold; -import org.netbeans.api.editor.fold.FoldHierarchy; import org.netbeans.api.editor.fold.FoldType; import org.netbeans.spi.editor.fold.FoldManager; +import static org.netbeans.editor.ext.java.Bundle.*; +import org.openide.util.NbBundle; + /** * Java fold maintainer creates and updates folds for java sources. * @@ -58,13 +59,23 @@ public abstract class JavaFoldManager implements FoldManager { - public static final FoldType INITIAL_COMMENT_FOLD_TYPE = new FoldType("initial-comment"); // NOI18N + public static final FoldType INITIAL_COMMENT_FOLD_TYPE = FoldType.INITIAL_COMMENT; // NOI18N - public static final FoldType IMPORTS_FOLD_TYPE = new FoldType("imports"); // NOI18N + @NbBundle.Messages("FoldType_Imports=Imports") + public static final FoldType IMPORTS_FOLD_TYPE = FoldType.create("import", FoldType_Imports(), + new org.netbeans.api.editor.fold.FoldTemplate(0, 0, "...")); // NOI18N - public static final FoldType JAVADOC_FOLD_TYPE = new FoldType("javadoc"); // NOI18N + @NbBundle.Messages("FoldType_Javadoc=Javadoc Comments") + public static final FoldType JAVADOC_FOLD_TYPE = FoldType.DOCUMENTATION.derive("javadoc", FoldType_Javadoc(), + new org.netbeans.api.editor.fold.FoldTemplate(3, 2, "/**...*/")); // NOI18N - public static final FoldType CODE_BLOCK_FOLD_TYPE = new FoldType("code-block"); // NOI18N + @NbBundle.Messages("FoldType_Methods=Methods") + public static final FoldType CODE_BLOCK_FOLD_TYPE = FoldType.MEMBER.derive("method", FoldType_Methods(), + new org.netbeans.api.editor.fold.FoldTemplate(1, 1, "{...}")); // NOI18N + + @NbBundle.Messages("FoldType_InnerClasses=Inner Classes") + public static final FoldType INNERCLASS_TYPE = FoldType.NESTED.derive("innerclass", "Inner Classes", + new org.netbeans.api.editor.fold.FoldTemplate(1, 1, "{...}")); // NOI18N private static final String IMPORTS_FOLD_DESCRIPTION = "..."; // NOI18N @@ -73,19 +84,27 @@ private static final String JAVADOC_FOLD_DESCRIPTION = "/**...*/"; // NOI18N private static final String CODE_BLOCK_FOLD_DESCRIPTION = "{...}"; // NOI18N - + + @Deprecated public static final FoldTemplate INITIAL_COMMENT_FOLD_TEMPLATE = new FoldTemplate(INITIAL_COMMENT_FOLD_TYPE, COMMENT_FOLD_DESCRIPTION, 2, 2); + @Deprecated public static final FoldTemplate IMPORTS_FOLD_TEMPLATE = new FoldTemplate(IMPORTS_FOLD_TYPE, IMPORTS_FOLD_DESCRIPTION, 0, 0); + @Deprecated public static final FoldTemplate JAVADOC_FOLD_TEMPLATE = new FoldTemplate(JAVADOC_FOLD_TYPE, JAVADOC_FOLD_DESCRIPTION, 3, 2); + @Deprecated public static final FoldTemplate CODE_BLOCK_FOLD_TEMPLATE = new FoldTemplate(CODE_BLOCK_FOLD_TYPE, CODE_BLOCK_FOLD_DESCRIPTION, 1, 1); + @Deprecated + public static final FoldTemplate INNER_CLASS_FOLD_TEMPLATE + = new FoldTemplate(INNERCLASS_TYPE, CODE_BLOCK_FOLD_DESCRIPTION, 1, 1); + protected static final class FoldTemplate { diff --git a/java.editor/nbproject/project.properties b/java.editor/nbproject/project.properties --- a/java.editor/nbproject/project.properties +++ b/java.editor/nbproject/project.properties @@ -42,7 +42,7 @@ javadoc.title=Java Editor -spec.version.base=2.51.0 +spec.version.base=2.52.0 test.qa-functional.cp.extra=${editor.dir}/modules/org-netbeans-modules-editor-fold.jar javac.source=1.6 #test.unit.cp.extra= diff --git a/java.editor/nbproject/project.xml b/java.editor/nbproject/project.xml --- a/java.editor/nbproject/project.xml +++ b/java.editor/nbproject/project.xml @@ -143,6 +143,7 @@ 1 + 1.34 @@ -205,7 +206,7 @@ 1 - 1.9 + 1.29 diff --git a/java.editor/src/org/netbeans/modules/editor/java/NbJavaCodeFoldingSideBarFactory.java b/java.editor/src/org/netbeans/modules/editor/java/NbJavaCodeFoldingSideBarFactory.java --- a/java.editor/src/org/netbeans/modules/editor/java/NbJavaCodeFoldingSideBarFactory.java +++ b/java.editor/src/org/netbeans/modules/editor/java/NbJavaCodeFoldingSideBarFactory.java @@ -50,8 +50,12 @@ /** * Java Code Folding Side Bar Factory, responsible for creating CodeFoldingSideBar * Plugged via layer.xml + *

+ * As this factory does not provide any extra functionality over the standard factory, + * it should be probably removed. * * @author Martin Roskanin + * @Deprecated */ public class NbJavaCodeFoldingSideBarFactory implements SideBarFactory{ diff --git a/java.editor/src/org/netbeans/modules/java/editor/fold/JavaElementFoldManager.java b/java.editor/src/org/netbeans/modules/java/editor/fold/JavaElementFoldManager.java --- a/java.editor/src/org/netbeans/modules/java/editor/fold/JavaElementFoldManager.java +++ b/java.editor/src/org/netbeans/modules/java/editor/fold/JavaElementFoldManager.java @@ -71,6 +71,7 @@ import javax.swing.text.Document; import javax.swing.text.Position; import org.netbeans.api.editor.fold.Fold; +import org.netbeans.spi.editor.fold.FoldInfo; import org.netbeans.api.editor.mimelookup.MimeLookup; import org.netbeans.api.editor.settings.SimpleValueNames; import org.netbeans.api.java.lexer.JavaTokenId; @@ -101,45 +102,8 @@ private FileObject file; private JavaElementFoldTask task; - /** - * Default folding of individual fold types. New instance is created - * when a new FoldManager opens (= editor appears). - */ - private static class Presets { - // Folding presets - private boolean foldImportsPreset = false; - private boolean foldInnerClassesPreset = false; - private boolean foldJavadocsPreset = false; - private boolean foldCodeBlocksPreset = false; - private boolean foldInitialCommentsPreset = false; - - public Presets() { - Preferences prefs = MimeLookup.getLookup(JavaKit.JAVA_MIME_TYPE).lookup(Preferences.class); - foldInitialCommentsPreset = prefs.getBoolean(SimpleValueNames.CODE_FOLDING_COLLAPSE_INITIAL_COMMENT, foldInitialCommentsPreset); - foldImportsPreset = prefs.getBoolean(SimpleValueNames.CODE_FOLDING_COLLAPSE_IMPORT, foldImportsPreset); - foldCodeBlocksPreset = prefs.getBoolean(SimpleValueNames.CODE_FOLDING_COLLAPSE_METHOD, foldCodeBlocksPreset); - foldInnerClassesPreset = prefs.getBoolean(SimpleValueNames.CODE_FOLDING_COLLAPSE_INNERCLASS, foldInnerClassesPreset); - foldJavadocsPreset = prefs.getBoolean(SimpleValueNames.CODE_FOLDING_COLLAPSE_JAVADOC, foldJavadocsPreset); - } - - private static volatile Presets CURRENT; - - static void refresh() { - CURRENT = null; - } - - static Presets get() { - Presets p = CURRENT; - if (p != null) { - return p; - } - return CURRENT = new Presets(); - } - } - public void init(FoldOperation operation) { this.operation = operation; - Presets.refresh(); } public synchronized void initFolds(FoldHierarchyTransaction transaction) { @@ -149,7 +113,6 @@ if (od instanceof DataObject) { FileObject file = ((DataObject)od).getPrimaryFile(); - currentFolds = new ArrayList(); task = JavaElementFoldTask.getTask(file); task.setJavaElementFoldManager(JavaElementFoldManager.this, file); } @@ -169,7 +132,6 @@ } public void removeDamagedNotify(Fold damagedFold) { - currentFolds.remove(operation.getExtraInfo(damagedFold)); if (importsFold == damagedFold) { importsFold = null;//not sure if this is correct... } @@ -187,7 +149,6 @@ task = null; file = null; - currentFolds = null; importsFold = null; initialCommentFold = null; } @@ -278,7 +239,7 @@ } final JavaElementFoldVisitor v = new JavaElementFoldVisitor(info, - cu, info.getTrees().getSourcePositions(), doc, Presets.get()); + cu, info.getTrees().getSourcePositions(), doc); scan(v, cu, null); @@ -291,15 +252,14 @@ if (v.stopped || isCancelled()) return ; - Collections.sort(v.folds); if (mgrs instanceof JavaElementFoldManager) { - SwingUtilities.invokeLater(((JavaElementFoldManager)mgrs).new CommitFolds(doc, v.folds)); + SwingUtilities.invokeLater(((JavaElementFoldManager)mgrs).createCommit(doc, v)); } else { SwingUtilities.invokeLater(new Runnable() { Collection jefms = (Collection)mgrs; public void run() { for (JavaElementFoldManager jefm : jefms) { - jefm.new CommitFolds(doc, v.folds).run(); + jefm.createCommit(doc, v).run(); } }}); } @@ -312,16 +272,24 @@ } + private CommitFolds createCommit(Document doc, JavaElementFoldVisitor v) { + return new CommitFolds(doc, v.folds, v.initialCommentInfo, v.importsInfo); + } + private class CommitFolds implements Runnable { private boolean insideRender; private Document doc; private List infos; private long startTime; + private FoldInfo initComment; + private FoldInfo imports; - public CommitFolds(Document doc, List infos) { + public CommitFolds(Document doc, List infos, FoldInfo initComment, FoldInfo imports) { this.doc = doc; this.infos = infos; + this.imports = imports; + this.initComment = initComment; } /** @@ -329,26 +297,11 @@ * ignores the default state, and takes it from the actual state of * existing fold. */ - private boolean mergeSpecialFoldState(FoldInfo fi) { - if (fi.template == IMPORTS_FOLD_TEMPLATE) { - if (importsFold != null) { - return importsFold.isCollapsed(); - } - } else if (fi.template == INITIAL_COMMENT_FOLD_TEMPLATE) { - if (initialCommentFold != null) { - return initialCommentFold.isCollapsed(); - } - } - return fi.collapseByDefault; - } - - private int flip(int order) { - if (order > 0) { - return -1; - } else if (order < 0) { - return 1; + private void setSpecialFoldState(FoldInfo info, Fold f) { + if (info == null || f == null) { + return; } else { - return 0; + info.collapsed(f.isCollapsed()); } } @@ -356,6 +309,10 @@ if (!insideRender) { startTime = System.currentTimeMillis(); insideRender = true; + + // retain import & initial comment states + setSpecialFoldState(imports, importsFold); + setSpecialFoldState(initComment, initialCommentFold); Document d = operation.getHierarchy().getComponent().getDocument(); if (d != doc) { return; @@ -366,92 +323,20 @@ } operation.getHierarchy().lock(); - try { - FoldHierarchyTransaction tr = operation.openTransaction(); - - try { - if (currentFolds == null) - return ; - - List updatedFolds = new ArrayList(infos.size()); - Iterator itExisting = currentFolds.iterator(); - Iterator itNew = infos.iterator(); - Fold currentExisting = itExisting.hasNext() ? itExisting.next() : null; - FoldInfo currentNew = itNew.hasNext() ? itNew.next() : null; - - while (currentExisting != null || currentNew != null) { - int order = currentExisting != null && currentNew != null ? currentNew.compareTo(currentExisting) : currentExisting != null ? 1 : -1; - - if (order > 0) { - //fold removed: - operation.removeFromHierarchy(currentExisting, tr); - - if (importsFold == currentExisting) { - importsFold = null; - } - - if (initialCommentFold == currentExisting) { - initialCommentFold = null; - } - - currentExisting = itExisting.hasNext() ? itExisting.next() : null; - } else { - //added or remains: - if (order < 0) { - //added: - int start = currentNew.start.getOffset(); - int end = currentNew.end.getOffset(); - - if (end > start && - (end - start) >= (currentNew.template.getStartGuardedLength() + currentNew.template.getEndGuardedLength())) { - Fold f = operation.addToHierarchy(currentNew.template.getType(), - currentNew.template.getDescription(), - mergeSpecialFoldState(currentNew), - start, - end, - currentNew.template.getStartGuardedLength(), - currentNew.template.getEndGuardedLength(), - currentNew, - tr); - - if (currentNew.template == IMPORTS_FOLD_TEMPLATE) { - importsFold = f; - } - - if (currentNew.template == INITIAL_COMMENT_FOLD_TEMPLATE) { - initialCommentFold = f; - } - - updatedFolds.add(f); - } - } else { - updatedFolds.add(currentExisting); - currentExisting = itExisting.hasNext() ? itExisting.next() : null; - } - - FoldInfo newNew = itNew.hasNext() ? itNew.next() : null; - - // XXX: In some situations infos contains duplicate folds and we don't - // want to add the same multiple times. The situation that I came across - // was with having an empty enum subclass with javadoc. The javadoc fold - // was added twice - once from visitClass and second time from visitMethod - // for the node, which for some reason has the same offset as the - // the enum inner class. - while (newNew != null && currentNew.compareTo(newNew) == 0) { - newNew = itNew.hasNext() ? itNew.next() : null; - } - - currentNew = newNew; - } - } - - currentFolds = updatedFolds; - } catch (BadLocationException e) { - Exceptions.printStackTrace(e); - } finally { - tr.commit(); + Map folds = operation.update(infos, null, null); + if (initComment != null) { + initialCommentFold = folds.get(initComment); + } else { + initialCommentFold = null; } + if (imports != null) { + importsFold = folds.get(imports); + } else { + importsFold = null; + } + } catch (BadLocationException e) { + Exceptions.printStackTrace(e); } finally { operation.getHierarchy().unlock(); } @@ -464,27 +349,26 @@ } //@GuardedBy(FoldOperation.openTransaction()) - private List currentFolds; //in natural order private Fold initialCommentFold; private Fold importsFold; private static final class JavaElementFoldVisitor extends CancellableTreePathScanner { - private List folds = new ArrayList(); + private List folds = new ArrayList(); private CompilationInfo info; private CompilationUnitTree cu; private SourcePositions sp; private boolean stopped; private int initialCommentStopPos = Integer.MAX_VALUE; private Document doc; - private Presets presets; + private FoldInfo initialCommentInfo; + private FoldInfo importsInfo; - public JavaElementFoldVisitor(CompilationInfo info, CompilationUnitTree cu, SourcePositions sp, Document doc, Presets presets) { + public JavaElementFoldVisitor(CompilationInfo info, CompilationUnitTree cu, SourcePositions sp, Document doc) { this.info = info; this.cu = cu; this.sp = sp; this.doc = doc; - this.presets = presets; } public void checkInitialFold() { @@ -500,21 +384,10 @@ if (token.id() == JavaTokenId.BLOCK_COMMENT || token.id() == JavaTokenId.JAVADOC_COMMENT) { int startOffset = ts.offset(); - boolean collapsed = presets.foldInitialCommentsPreset; - - /* - if (initialCommentFold != null) { - collapsed = initialCommentFold.isCollapsed(); - } - */ - - folds.add(new FoldInfo(doc, startOffset, startOffset + token.length(), INITIAL_COMMENT_FOLD_TEMPLATE, collapsed)); + folds.add(initialCommentInfo = FoldInfo.range(startOffset, startOffset + token.length(), INITIAL_COMMENT_FOLD_TYPE)); break; } } - } catch (BadLocationException e) { - //the document probably changed, stop - stopped = true; } catch (ConcurrentModificationException e) { //from TokenSequence, document probably changed, stop stopped = true; @@ -542,7 +415,7 @@ if (token.id() == JavaTokenId.JAVADOC_COMMENT) { int startOffset = ts.offset(); - folds.add(new FoldInfo(doc, startOffset, startOffset + token.length(), JAVADOC_FOLD_TEMPLATE, presets.foldJavadocsPreset)); + folds.add(FoldInfo.range(startOffset, startOffset + token.length(), JAVADOC_FOLD_TYPE)); if (startOffset < initialCommentStopPos) initialCommentStopPos = startOffset; } @@ -559,8 +432,9 @@ int start = (int)sp.getStartPosition(cu, node); int end = (int)sp.getEndPosition(cu, node); - if (start != (-1) && end != (-1)) - folds.add(new FoldInfo(doc, start, end, CODE_BLOCK_FOLD_TEMPLATE, presets.foldCodeBlocksPreset)); + if (start != (-1) && end != (-1)) { + folds.add(FoldInfo.range(start, end, CODE_BLOCK_FOLD_TYPE)); + } } handleJavadoc(javadocTree != null ? javadocTree : node); @@ -588,8 +462,9 @@ int start = Utilities.findBodyStart(node, cu, sp, doc); int end = (int)sp.getEndPosition(cu, node); - if (start != (-1) && end != (-1)) - folds.add(new FoldInfo(doc, start, end, CODE_BLOCK_FOLD_TEMPLATE, presets.foldInnerClassesPreset)); + if (start != (-1) && end != (-1)) { + folds.add(FoldInfo.range(start, end, INNERCLASS_TYPE)); + } } handleJavadoc(node); @@ -642,99 +517,17 @@ } if (importsEnd != (-1) && importsStart != (-1)) { - if (importsStart < initialCommentStopPos) + if (importsStart < initialCommentStopPos) { initialCommentStopPos = importsStart; - - try { - boolean collapsed = presets.foldImportsPreset; - - /* - if (importsFold != null) { - collapsed = importsFold.isCollapsed(); - } - */ - - importsStart += 7/*"import ".length()*/; - - if (importsStart < importsEnd) { - folds.add(new FoldInfo(doc, importsStart , importsEnd, IMPORTS_FOLD_TEMPLATE, collapsed)); - } - } catch (BadLocationException e) { - //the document probably changed, stop - stopped = true; + } + importsStart += 7/*"import ".length()*/; + + if (importsStart < importsEnd) { + folds.add(importsInfo = FoldInfo.range(importsStart , importsEnd, IMPORTS_FOLD_TYPE)); } } return super.visitCompilationUnit(node, p); } - - } - - protected static final class FoldInfo implements Comparable { - - private final Position start; - private final Position end; - private final FoldTemplate template; - private final boolean collapseByDefault; - //@GUardedBy(FoldOperation.openTransaction()) - - public FoldInfo(Document doc, int start, int end, FoldTemplate template, boolean collapseByDefault) throws BadLocationException { - this.start = doc.createPosition(start); - this.end = doc.createPosition(end); - this.template = template; - this.collapseByDefault = collapseByDefault; - } - - public int compareTo(Fold remote) { - if (start.getOffset() < remote.getStartOffset()) { - return -1; - } - - if (start.getOffset() > remote.getStartOffset()) { - return 1; - } - - if (end.getOffset() < remote.getEndOffset()) { - return -1; - } - - if (end.getOffset() > remote.getEndOffset()) { - return 1; - } - - //XXX: abusing the length of the fold description to implement ordering (the exact order does not matter in this case): - return template.getDescription().length() - remote.getDescription().length(); - } - - public int compareTo(Object o) { - if (o instanceof Fold) { - return compareTo((Fold)o); - } - FoldInfo remote = (FoldInfo) o; - - if (start.getOffset() < remote.start.getOffset()) { - return -1; - } - - if (start.getOffset() > remote.start.getOffset()) { - return 1; - } - - if (end.getOffset() < remote.end.getOffset()) { - return -1; - } - - if (end.getOffset() > remote.end.getOffset()) { - return 1; - } - - //XXX: abusing the length of the fold description to implement ordering (the exact order does not matter in this case): - return template.getDescription().length() - remote.template.getDescription().length(); - } - - @Override - public String toString() { - return "FoldInfo[" + start.getOffset() + ", " + end.getOffset() + ", " + template.getDescription() + "]"; - } } } diff --git a/java.editor/src/org/netbeans/modules/java/editor/fold/JavaFoldTypeProvider.java b/java.editor/src/org/netbeans/modules/java/editor/fold/JavaFoldTypeProvider.java new file mode 100644 --- /dev/null +++ b/java.editor/src/org/netbeans/modules/java/editor/fold/JavaFoldTypeProvider.java @@ -0,0 +1,78 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2013 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + * + * Contributor(s): + * + * Portions Copyrighted 2013 Sun Microsystems, Inc. + */ +package org.netbeans.modules.java.editor.fold; + +import java.util.ArrayList; +import java.util.Collection; +import org.netbeans.api.editor.fold.FoldType; +import org.netbeans.api.editor.mimelookup.MimeRegistration; +import org.netbeans.editor.ext.java.JavaFoldManager; +import org.netbeans.spi.editor.fold.FoldTypeProvider; + +/** + * + * @author sdedic + */ +@MimeRegistration(mimeType = "text/x-java", service = FoldTypeProvider.class) +public class JavaFoldTypeProvider implements FoldTypeProvider { + private Collection types = new ArrayList(5); + + public JavaFoldTypeProvider() { + types.add(JavaFoldManager.CODE_BLOCK_FOLD_TYPE); + types.add(JavaFoldManager.INNERCLASS_TYPE); + types.add(JavaFoldManager.IMPORTS_FOLD_TYPE); + types.add(JavaFoldManager.JAVADOC_FOLD_TYPE); + types.add(JavaFoldManager.INITIAL_COMMENT_FOLD_TYPE); + } + + + @Override + public Collection getValues(Class type) { + return types; + } + + @Override + public boolean inheritable() { + return false; + } + +} diff --git a/java.editor/src/org/netbeans/modules/java/editor/fold/JavadocReaderFactory.java b/java.editor/src/org/netbeans/modules/java/editor/fold/JavadocReaderFactory.java new file mode 100644 --- /dev/null +++ b/java.editor/src/org/netbeans/modules/java/editor/fold/JavadocReaderFactory.java @@ -0,0 +1,64 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2013 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + * + * Contributor(s): + * + * Portions Copyrighted 2013 Sun Microsystems, Inc. + */ +package org.netbeans.modules.java.editor.fold; + +import org.netbeans.api.editor.fold.FoldType; +import org.netbeans.api.editor.fold.FoldUtilities; +import org.netbeans.api.editor.fold.FoldingSupport; +import org.netbeans.api.editor.mimelookup.MimeRegistration; +import org.netbeans.spi.editor.fold.ContentReader; + +/** + * + * @author sdedic + */ +@MimeRegistration(mimeType = "text/x-java", service = ContentReader.Factory.class) +public class JavadocReaderFactory implements ContentReader.Factory { + @Override + public ContentReader createReader(FoldType ft) { + if (ft == JavaElementFoldManager.JAVADOC_FOLD_TYPE) { + return FoldingSupport.contentReader("*", "\\.", "@", null); + } else { + return null; + } + } +} diff --git a/java.editor/src/org/netbeans/modules/java/editor/resources/layer.xml b/java.editor/src/org/netbeans/modules/java/editor/resources/layer.xml --- a/java.editor/src/org/netbeans/modules/java/editor/resources/layer.xml +++ b/java.editor/src/org/netbeans/modules/java/editor/resources/layer.xml @@ -141,13 +141,13 @@ - - - - + - + + + + diff --git a/options.editor/manifest.mf b/options.editor/manifest.mf --- a/options.editor/manifest.mf +++ b/options.editor/manifest.mf @@ -2,6 +2,6 @@ OpenIDE-Module: org.netbeans.modules.options.editor/1 OpenIDE-Module-Localizing-Bundle: org/netbeans/modules/options/editor/Bundle.properties OpenIDE-Module-Layer: org/netbeans/modules/options/editor/mf-layer.xml -OpenIDE-Module-Specification-Version: 1.39 +OpenIDE-Module-Specification-Version: 1.40 AutoUpdate-Show-In-Client: false diff --git a/options.editor/src/org/netbeans/modules/options/generaleditor/GeneralEditorPanel.form b/options.editor/src/org/netbeans/modules/options/generaleditor/GeneralEditorPanel.form --- a/options.editor/src/org/netbeans/modules/options/generaleditor/GeneralEditorPanel.form +++ b/options.editor/src/org/netbeans/modules/options/generaleditor/GeneralEditorPanel.form @@ -29,24 +29,11 @@ - - - + - - - - - - - - - - - - - + + @@ -54,10 +41,7 @@ - - - - + @@ -72,11 +56,6 @@ - - - - - @@ -97,33 +76,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -154,64 +106,12 @@ - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/options.editor/src/org/netbeans/modules/options/generaleditor/GeneralEditorPanel.java b/options.editor/src/org/netbeans/modules/options/generaleditor/GeneralEditorPanel.java --- a/options.editor/src/org/netbeans/modules/options/generaleditor/GeneralEditorPanel.java +++ b/options.editor/src/org/netbeans/modules/options/generaleditor/GeneralEditorPanel.java @@ -76,18 +76,6 @@ public GeneralEditorPanel () { initComponents (); - loc (lCodeFolding, "Code_Folding"); - loc (lUseCodeFolding, "Code_Folding_Section"); - loc (lCollapseByDefault, "Fold_by_Default"); - - loc (cbUseCodeFolding, "Use_Folding"); - loc (cbFoldMethods, "Fold_Methods"); - loc (cbFoldInnerClasses, "Fold_Classes"); - loc (cbFoldImports, "Fold_Imports"); - loc (cbFoldJavadocComments, "Fold_JavaDoc"); - loc (cbFoldInitialComments, "Fold_Licence"); - loc (cbFoldTags, "Fold_Tags"); - loc (lCamelCaseBehavior, "Camel_Case_Behavior"); loc (cbCamelCaseBehavior, "Enable_Camel_Case_In_Java"); loc (lCamelCaseBehaviorExample, "Camel_Case_Behavior_Example"); @@ -99,7 +87,6 @@ loc (cbBraceTooltip, "Brace_First_Tooltip"); loc (cbShowBraceOutline, "Brace_Show_Outline"); - cbUseCodeFolding.setMnemonic(NbBundle.getMessage (GeneralEditorPanel.class, "MNEMONIC_Use_Folding").charAt(0)); cboEditorSearchType.setRenderer(new EditorSearchTypeRenderer(cboEditorSearchType.getRenderer())); cboEditorSearchType.setModel(new DefaultComboBoxModel(new Object [] { "default", "closing"})); //NOI18N cboEditorSearchType.addActionListener( new ActionListener() { @@ -123,17 +110,6 @@ // //GEN-BEGIN:initComponents private void initComponents() { - lCodeFolding = new javax.swing.JLabel(); - lUseCodeFolding = new javax.swing.JLabel(); - lCollapseByDefault = new javax.swing.JLabel(); - cbUseCodeFolding = new javax.swing.JCheckBox(); - cbFoldMethods = new javax.swing.JCheckBox(); - cbFoldInnerClasses = new javax.swing.JCheckBox(); - cbFoldImports = new javax.swing.JCheckBox(); - cbFoldJavadocComments = new javax.swing.JCheckBox(); - cbFoldInitialComments = new javax.swing.JCheckBox(); - cbFoldTags = new javax.swing.JCheckBox(); - jSeparator1 = new javax.swing.JSeparator(); lBracesMatching = new javax.swing.JLabel(); cbShowBraceOutline = new javax.swing.JCheckBox(); cbBraceTooltip = new javax.swing.JCheckBox(); @@ -150,25 +126,6 @@ setForeground(new java.awt.Color(99, 130, 191)); - lCodeFolding.setText("Code Folding"); - - lUseCodeFolding.setLabelFor(cbUseCodeFolding); - lUseCodeFolding.setText("Use Code Folding:"); - - lCollapseByDefault.setText("Collapse by Default:"); - - cbFoldMethods.setText("Methods"); - - cbFoldInnerClasses.setText("Inner Classes"); - - cbFoldImports.setText("Imports"); - - cbFoldJavadocComments.setText("Javadoc Comments"); - - cbFoldInitialComments.setText("Initial Comments"); - - cbFoldTags.setText("Tags and Other Code Blocks"); - lBracesMatching.setText(org.openide.util.NbBundle.getMessage(GeneralEditorPanel.class, "BRACES_MATCHING")); // NOI18N cbShowBraceOutline.setText("Show outline"); @@ -200,29 +157,16 @@ .addGroup(layout.createSequentialGroup() .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(layout.createSequentialGroup() - .addContainerGap() - .addComponent(lCollapseByDefault) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGap(163, 163, 163) .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(cbUseCodeFolding) .addGroup(layout.createSequentialGroup() - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(cbFoldMethods) - .addComponent(cbFoldInnerClasses) - .addComponent(cbFoldImports)) - .addGap(18, 18, 18) - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(cbFoldJavadocComments) - .addComponent(cbFoldInitialComments) - .addComponent(cbFoldTags) - .addComponent(cbBraceTooltip))) + .addGap(175, 175, 175) + .addComponent(cbBraceTooltip)) .addComponent(cbCamelCaseBehavior) .addGroup(layout.createSequentialGroup() .addGap(21, 21, 21) - .addComponent(lCamelCaseBehaviorExample, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)))) - .addGroup(layout.createSequentialGroup() - .addGap(12, 12, 12) - .addComponent(lUseCodeFolding)) + .addComponent(lCamelCaseBehaviorExample, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))) + .addGap(26, 26, 26)) .addGroup(layout.createSequentialGroup() .addContainerGap() .addComponent(lEditorSearchType) @@ -232,10 +176,6 @@ .addComponent(cboEditorSearchType, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)))) .addGap(0, 12, Short.MAX_VALUE)) .addGroup(layout.createSequentialGroup() - .addComponent(lCodeFolding) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(jSeparator1)) - .addGroup(layout.createSequentialGroup() .addComponent(lBracesMatching) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(jSeparator6)) @@ -248,34 +188,9 @@ .addGap(0, 0, Short.MAX_VALUE)) .addComponent(jSeparator3))) ); - - layout.linkSize(javax.swing.SwingConstants.HORIZONTAL, new java.awt.Component[] {cbFoldImports, cbFoldInitialComments, cbFoldInnerClasses, cbFoldJavadocComments, cbFoldMethods}); - layout.setVerticalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(layout.createSequentialGroup() - .addContainerGap() - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING) - .addComponent(lCodeFolding) - .addComponent(jSeparator1, javax.swing.GroupLayout.PREFERRED_SIZE, 10, javax.swing.GroupLayout.PREFERRED_SIZE)) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(cbUseCodeFolding) - .addComponent(lUseCodeFolding)) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) - .addComponent(lCollapseByDefault) - .addComponent(cbFoldMethods) - .addComponent(cbFoldJavadocComments)) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) - .addComponent(cbFoldInnerClasses) - .addComponent(cbFoldInitialComments)) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) - .addComponent(cbFoldImports) - .addComponent(cbFoldTags)) - .addGap(18, 18, 18) .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING) .addComponent(lBracesMatching) .addComponent(jSeparator6, javax.swing.GroupLayout.PREFERRED_SIZE, 10, javax.swing.GroupLayout.PREFERRED_SIZE)) @@ -301,7 +216,7 @@ .addComponent(cboEditorSearchType, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(lSearchtypeTooltip, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addContainerGap()) + .addGap(0, 12, Short.MAX_VALUE)) ); }// //GEN-END:initComponents @@ -309,28 +224,17 @@ // Variables declaration - do not modify//GEN-BEGIN:variables private javax.swing.JCheckBox cbBraceTooltip; private javax.swing.JCheckBox cbCamelCaseBehavior; - private javax.swing.JCheckBox cbFoldImports; - private javax.swing.JCheckBox cbFoldInitialComments; - private javax.swing.JCheckBox cbFoldInnerClasses; - private javax.swing.JCheckBox cbFoldJavadocComments; - private javax.swing.JCheckBox cbFoldMethods; - private javax.swing.JCheckBox cbFoldTags; private javax.swing.JCheckBox cbShowBraceOutline; - private javax.swing.JCheckBox cbUseCodeFolding; private javax.swing.JComboBox cboEditorSearchType; - private javax.swing.JSeparator jSeparator1; private javax.swing.JSeparator jSeparator3; private javax.swing.JSeparator jSeparator5; private javax.swing.JSeparator jSeparator6; private javax.swing.JLabel lBracesMatching; private javax.swing.JLabel lCamelCaseBehavior; private javax.swing.JLabel lCamelCaseBehaviorExample; - private javax.swing.JLabel lCodeFolding; - private javax.swing.JLabel lCollapseByDefault; private javax.swing.JLabel lEditorSearchType; private javax.swing.JLabel lSearch; private javax.swing.JLabel lSearchtypeTooltip; - private javax.swing.JLabel lUseCodeFolding; // End of variables declaration//GEN-END:variables @@ -362,28 +266,12 @@ listen = false; if (model == null) { model = new Model (); - cbUseCodeFolding.addActionListener (this); - cbFoldMethods.addActionListener (this); - cbFoldInnerClasses.addActionListener (this); - cbFoldImports.addActionListener (this); - cbFoldJavadocComments.addActionListener (this); - cbFoldInitialComments.addActionListener (this); cbCamelCaseBehavior.addActionListener (this); - cbFoldTags.addActionListener (this); cboEditorSearchType.addActionListener(this); cbBraceTooltip.addActionListener(this); cbShowBraceOutline.addActionListener(this); } - // init code folding - cbUseCodeFolding.setSelected (model.isShowCodeFolding ()); - cbFoldImports.setSelected (model.isFoldImports ()); - cbFoldInitialComments.setSelected (model.isFoldInitialComment ()); - cbFoldInnerClasses.setSelected (model.isFoldInnerClasses ()); - cbFoldJavadocComments.setSelected (model.isFoldJavaDocComments ()); - cbFoldMethods.setSelected (model.isFoldMethods ()); - cbFoldTags.setSelected (model.isFoldTag()); - // Java Camel Case Navigation Boolean ccJava = model.isCamelCaseJavaNavigation(); if ( ccJava == null ) { @@ -400,8 +288,6 @@ cbBraceTooltip.setSelected(model.isBraceTooltip()); cbShowBraceOutline.setSelected(model.isBraceOutline()); - updateEnabledState (); - listen = true; } @@ -409,17 +295,6 @@ if (model == null || !changed) return; - // code folding options - model.setFoldingOptions ( - cbUseCodeFolding.isSelected (), - cbFoldImports.isSelected (), - cbFoldInitialComments.isSelected (), - cbFoldInnerClasses.isSelected (), - cbFoldJavadocComments.isSelected (), - cbFoldMethods.isSelected (), - cbFoldTags.isSelected () - ); - // java camel case navigation model.setCamelCaseNavigation(cbCamelCaseBehavior.isSelected()); @@ -446,25 +321,12 @@ @Override public void actionPerformed (ActionEvent e) { if (!listen) return; - if (e.getSource () == cbUseCodeFolding) { - updateEnabledState (); - } changed = true; } // other methods ........................................................... - private void updateEnabledState () { - boolean useCodeFolding = cbUseCodeFolding.isSelected (); - cbFoldImports.setEnabled (useCodeFolding); - cbFoldInitialComments.setEnabled (useCodeFolding); - cbFoldInnerClasses.setEnabled (useCodeFolding); - cbFoldJavadocComments.setEnabled (useCodeFolding); - cbFoldMethods.setEnabled (useCodeFolding); - cbFoldTags.setEnabled(useCodeFolding); - } - private static final class EditorSearchTypeRenderer implements ListCellRenderer { private final ListCellRenderer defaultRenderer; diff --git a/options.editor/src/org/netbeans/modules/options/indentation/ProxyPreferences.java b/options.editor/src/org/netbeans/modules/options/indentation/ProxyPreferences.java --- a/options.editor/src/org/netbeans/modules/options/indentation/ProxyPreferences.java +++ b/options.editor/src/org/netbeans/modules/options/indentation/ProxyPreferences.java @@ -44,482 +44,229 @@ import java.io.IOException; import java.io.OutputStream; -import java.lang.ref.Reference; -import java.lang.ref.WeakReference; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.EventObject; import java.util.HashMap; import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; import java.util.Map; import java.util.Set; -import java.util.WeakHashMap; -import java.util.logging.Level; -import java.util.logging.Logger; import java.util.prefs.BackingStoreException; import java.util.prefs.NodeChangeEvent; import java.util.prefs.NodeChangeListener; import java.util.prefs.PreferenceChangeEvent; import java.util.prefs.PreferenceChangeListener; import java.util.prefs.Preferences; -import javax.xml.bind.DatatypeConverter; -import org.netbeans.modules.editor.settings.storage.spi.TypedValue; +import org.netbeans.modules.editor.settings.storage.api.MemoryPreferences; import org.openide.util.WeakListeners; /** - * + * This class has been obsoleted by {@link MemoryPreferences} + * * @author vita + * @deprecated Please use {@link MemoryPreferences} API instead */ public final class ProxyPreferences extends Preferences implements PreferenceChangeListener, NodeChangeListener { + + private final MemoryPreferences delegateRoot; + private Preferences delegate; + private boolean noEvents; + + public static ProxyPreferences getProxyPreferences(Object token, Preferences delegate) { + return new ProxyPreferences(null, MemoryPreferences.get(token, delegate), null); + } - public static ProxyPreferences getProxyPreferences(Object token, Preferences delegate) { - return Tree.getTree(token, delegate).get(null, delegate.name(), delegate); //NOI18N + public void silence() { + synchronized (delegate) { + this.noEvents = true; + } + } + + public void destroy() { + delegateRoot.destroy(); } @Override public void put(String key, String value) { - _put(key, value, String.class.getName()); + delegate.put(key, value); } @Override public String get(String key, String def) { - synchronized (tree.treeLock()) { - checkNotNull(key, "key"); //NOI18N - checkRemoved(); - - if (removedKeys.contains(key)) { - if (LOG.isLoggable(Level.FINE)) { - LOG.fine("Key '" + key + "' removed, using default '" + def + "'"); //NOI18N - } - return def; - } else { - TypedValue typedValue = data.get(key); - if (typedValue != null) { - if (LOG.isLoggable(Level.FINE)) { - LOG.fine("Key '" + key + "' modified, local value '" + typedValue.getValue() + "'"); //NOI18N - } - return typedValue.getValue(); - } else if (delegate != null) { - String value = delegate.get(key, def); - if (LOG.isLoggable(Level.FINE)) { - LOG.fine("Key '" + key + "' undefined, original value '" + value + "'"); //NOI18N - } - return value; - } else { - if (LOG.isLoggable(Level.FINE)) { - LOG.fine("Key '" + key + "' undefined, '" + name + "' is a new node, using default '" + def + "'"); //NOI18N - } - return def; - } + return delegate.get(key, def); + } + + @Override + public void remove(String key) { + delegate.remove(key); + } + + @Override + public void clear() throws BackingStoreException { + delegate.clear(); + } + + @Override + public void putInt(String key, int value) { + delegate.putInt(key, value); + } + + @Override + public int getInt(String key, int def) { + return delegate.getInt(key, def); + } + + @Override + public void putLong(String key, long value) { + delegate.putLong(key, value); + } + + @Override + public long getLong(String key, long def) { + return delegate.getLong(key, def); + } + + @Override + public void putBoolean(String key, boolean value) { + delegate.putBoolean(key, value); + } + + @Override + public boolean getBoolean(String key, boolean def) { + return delegate.getBoolean(key, def); + } + + @Override + public void putFloat(String key, float value) { + delegate.putFloat(key, value); + } + + @Override + public float getFloat(String key, float def) { + return delegate.getFloat(key, def); + } + + @Override + public void putDouble(String key, double value) { + delegate.putDouble(key, value); + } + + @Override + public double getDouble(String key, double def) { + return delegate.getDouble(key, def); + } + + @Override + public void putByteArray(String key, byte[] value) { + delegate.putByteArray(key, value); + } + + @Override + public byte[] getByteArray(String key, byte[] def) { + return delegate.getByteArray(key, def); + } + + @Override + public String[] keys() throws BackingStoreException { + return delegate.keys(); + } + + @Override + public String[] childrenNames() throws BackingStoreException { + return delegate.childrenNames(); + } + + @Override + public Preferences parent() { + return parent; + } + + @Override + public Preferences node(String pathName) { + synchronized (this) { + Preferences pref = children.get(pathName); + if (pref != null) { + return pref; } } + Preferences pref = delegate.node(pathName); + ProxyPreferences result; + + synchronized (this) { + result = children.get(pathName); + if (result != null) { + return result; + } + result = new ProxyPreferences(this, delegateRoot, pref); + children.put(pathName, result); + return result; + } } @Override - public void remove(String key) { - EventBag bag = null; - - synchronized (tree.treeLock()) { - checkNotNull(key, "key"); //NOI18N - checkRemoved(); - - if (removedKeys.add(key)) { - data.remove(key); - bag = new EventBag(); - bag.addListeners(prefListeners); - bag.addEvent(new PreferenceChangeEvent(this, key, null)); - } - } - - if (bag != null) { - firePrefEvents(Collections.singletonList(bag)); - } - } - - @Override - public void clear() throws BackingStoreException { - EventBag bag = new EventBag(); - - synchronized (tree.treeLock()) { - checkRemoved(); - - // Determine modified or added keys - Set keys = new HashSet(); - keys.addAll(data.keySet()); - keys.removeAll(removedKeys); - if (!keys.isEmpty()) { - for(String key : keys) { - String value = delegate == null ? null : delegate.get(key, null); - bag.addEvent(new PreferenceChangeEvent(this, key, value)); - } - } - - // Determine removed keys - if (delegate != null) { - for(String key : removedKeys) { - String value = delegate.get(key, null); - if (value != null) { - bag.addEvent(new PreferenceChangeEvent(this, key, value)); - } - } - } - - // Initialize bag's listeners - bag.addListeners(prefListeners); - - // Finally, remove the data - data.clear(); - removedKeys.clear(); - } - - firePrefEvents(Collections.singletonList(bag)); - } - - @Override - public void putInt(String key, int value) { - _put(key, Integer.toString(value), Integer.class.getName()); - } - - @Override - public int getInt(String key, int def) { - String value = get(key, null); - if (value != null) { - try { - return Integer.parseInt(value); - } catch (NumberFormatException nfe) { - // ignore - } - } - return def; - } - - @Override - public void putLong(String key, long value) { - _put(key, Long.toString(value), Long.class.getName()); - } - - @Override - public long getLong(String key, long def) { - String value = get(key, null); - if (value != null) { - try { - return Long.parseLong(value); - } catch (NumberFormatException nfe) { - // ignore - } - } - return def; - } - - @Override - public void putBoolean(String key, boolean value) { - _put(key, Boolean.toString(value), Boolean.class.getName()); - } - - @Override - public boolean getBoolean(String key, boolean def) { - String value = get(key, null); - if (value != null) { - return Boolean.parseBoolean(value); - } else { - return def; - } - } - - @Override - public void putFloat(String key, float value) { - _put(key, Float.toString(value), Float.class.getName()); - } - - @Override - public float getFloat(String key, float def) { - String value = get(key, null); - if (value != null) { - try { - return Float.parseFloat(value); - } catch (NumberFormatException nfe) { - // ignore - } - } - return def; - } - - @Override - public void putDouble(String key, double value) { - _put(key, Double.toString(value), Double.class.getName()); - } - - @Override - public double getDouble(String key, double def) { - String value = get(key, null); - if (value != null) { - try { - return Double.parseDouble(value); - } catch (NumberFormatException nfe) { - // ignore - } - } - return def; - } - - @Override - public void putByteArray(String key, byte[] value) { - _put(key, DatatypeConverter.printBase64Binary(value), value.getClass().getName()); - } - - @Override - public byte[] getByteArray(String key, byte[] def) { - String value = get(key, null); - if (value != null) { - byte [] decoded = DatatypeConverter.parseBase64Binary(value); - if (decoded != null) { - return decoded; - } - } - return def; - } - - @Override - public String[] keys() throws BackingStoreException { - synchronized (tree.treeLock()) { - checkRemoved(); - HashSet keys = new HashSet(); - if (delegate != null) { - keys.addAll(Arrays.asList(delegate.keys())); - } - keys.addAll(data.keySet()); - keys.removeAll(removedKeys); - return keys.toArray(new String [keys.size()]); - } - } - - @Override - public String[] childrenNames() throws BackingStoreException { - synchronized (tree.treeLock()) { - checkRemoved(); - HashSet names = new HashSet(); - if (delegate != null) { - names.addAll(Arrays.asList(delegate.childrenNames())); - } - names.addAll(children.keySet()); - names.removeAll(removedChildren); - return names.toArray(new String [names.size()]); - } - } - - @Override - public Preferences parent() { - synchronized (tree.treeLock()) { - checkRemoved(); - return parent; - } - } - - @Override - public Preferences node(String pathName) { - Preferences node; - LinkedList> events = new LinkedList>(); - - synchronized (tree.treeLock()) { - checkNotNull(pathName, "pathName"); //NOI18N - checkRemoved(); - node = node(pathName, true, events); - } - - fireNodeEvents(events); - return node; - } - - @Override public boolean nodeExists(String pathName) throws BackingStoreException { - synchronized (tree.treeLock()) { - if (pathName.length() == 0) { - return !removed; - } else { - checkRemoved(); - return node(pathName, false, null) != null; - } - } + return delegate.nodeExists(pathName); } @Override public void removeNode() throws BackingStoreException { - synchronized (tree.treeLock()) { - checkRemoved(); - ProxyPreferences p = parent; - if (p != null) { - p.removeChild(this); - } else { - throw new UnsupportedOperationException("Can't remove the root."); //NOI18N - } + delegate.removeNode(); + if (parent != null) { + parent.nodeRemoved(this); } } @Override public String name() { - return name; + return delegate.name(); } @Override public String absolutePath() { - synchronized (tree.treeLock()) { - ProxyPreferences pp = parent; - if (pp != null) { - if (pp.parent == null) { - // pp is the root, we don't want two consecutive slashes in the path - return "/" + name(); //NOI18N - } else { - return pp.absolutePath() + "/" + name(); //NOI18N - } - } else { - return "/"; //NOI18N - } - } + return delegate.absolutePath(); } @Override public boolean isUserNode() { - synchronized (tree.treeLock()) { - if (delegate != null) { - return delegate.isUserNode(); - } else { - ProxyPreferences pp = parent; - if (pp != null) { - return pp.isUserNode(); - } else { - return true; - } - } - } + return delegate.isUserNode(); } @Override public String toString() { - return (isUserNode() ? "User" : "System") + " Preference Node: " + absolutePath(); //NOI18N + return delegate.toString(); } @Override public void flush() throws BackingStoreException { - synchronized (tree.treeLock()) { - if (LOG.isLoggable(Level.FINE)) { - LOG.fine("Flushing " + absolutePath()); - } - - checkRemoved(); - for(ProxyPreferences pp : children.values()) { - pp.flush(); - } - - if (delegate == null) { - ProxyPreferences proxyRoot = parent.node("/", false, null); //NOI18N - assert proxyRoot != null : "Root must always exist"; //NOI18N - - Preferences delegateRoot = proxyRoot.delegate; - assert delegateRoot != null : "Root must always have its corresponding delegate"; //NOI18N - - Preferences nueDelegate = delegateRoot.node(absolutePath()); - changeDelegate(nueDelegate); - } - - delegate.removeNodeChangeListener(weakNodeListener); - delegate.removePreferenceChangeListener(weakPrefListener); - try { - // remove all removed children - for(String childName : removedChildren) { - if (delegate.nodeExists(childName)) { - delegate.node(childName).removeNode(); - } - } - - // write all valid key-value pairs - for(String key : data.keySet()) { - if (!removedKeys.contains(key)) { - if (LOG.isLoggable(Level.FINE)) { - LOG.fine("Writing " + absolutePath() + "/" + key + "=" + data.get(key)); - } - - TypedValue typedValue = data.get(key); - if (String.class.getName().equals(typedValue.getJavaType())) { - delegate.put(key, typedValue.getValue()); - - } else if (Integer.class.getName().equals(typedValue.getJavaType())) { - delegate.putInt(key, Integer.parseInt(typedValue.getValue())); - - } else if (Long.class.getName().equals(typedValue.getJavaType())) { - delegate.putLong(key, Long.parseLong(typedValue.getValue())); - - } else if (Boolean.class.getName().equals(typedValue.getJavaType())) { - delegate.putBoolean(key, Boolean.parseBoolean(typedValue.getValue())); - - } else if (Float.class.getName().equals(typedValue.getJavaType())) { - delegate.putFloat(key, Float.parseFloat(typedValue.getValue())); - - } else if (Double.class.getName().equals(typedValue.getJavaType())) { - delegate.putDouble(key, Double.parseDouble(typedValue.getValue())); - - } else { - delegate.putByteArray(key, DatatypeConverter.parseBase64Binary(typedValue.getValue())); - } - } - } - data.clear(); - - // remove all removed keys - for(String key : removedKeys) { - if (LOG.isLoggable(Level.FINE)) { - LOG.fine("Removing " + absolutePath() + "/" + key); - } - delegate.remove(key); - } - removedKeys.clear(); - } finally { - delegate.addNodeChangeListener(weakNodeListener); - delegate.addPreferenceChangeListener(weakPrefListener); - } - } + delegate.flush(); } @Override public void sync() throws BackingStoreException { - ArrayList> prefEvents = new ArrayList>(); - ArrayList> nodeEvents = new ArrayList>(); - - synchronized (tree.treeLock()) { - _sync(prefEvents, nodeEvents); - } - - fireNodeEvents(nodeEvents); - firePrefEvents(prefEvents); + delegate.sync(); } - + @Override public void addPreferenceChangeListener(PreferenceChangeListener pcl) { - synchronized (tree.treeLock()) { + synchronized (this) { prefListeners.add(pcl); } } @Override public void removePreferenceChangeListener(PreferenceChangeListener pcl) { - synchronized (tree.treeLock()) { + synchronized (this) { prefListeners.remove(pcl); } } @Override public void addNodeChangeListener(NodeChangeListener ncl) { - synchronized (tree.treeLock()) { + synchronized (this) { nodeListeners.add(ncl); } } @Override public void removeNodeChangeListener(NodeChangeListener ncl) { - synchronized (tree.treeLock()) { + synchronized (this) { nodeListeners.remove(ncl); } } @@ -540,11 +287,10 @@ public void preferenceChange(PreferenceChangeEvent evt) { PreferenceChangeListener [] listeners; - synchronized (tree.treeLock()) { - if (removed || removedKeys.contains(evt.getKey()) || data.containsKey(evt.getKey())) { + synchronized (this) { + if (!noEvents) { return; } - listeners = prefListeners.toArray(new PreferenceChangeListener[prefListeners.size()]); } @@ -556,6 +302,14 @@ l.preferenceChange(myEvt); } } + + private synchronized void changeDelegate(Preferences del) { + this.delegate = del; + } + + private synchronized void nodeRemoved(ProxyPreferences child) { + children.values().remove(child); + } // ------------------------------------------------------------------------ // NodeChangeListener implementation @@ -565,12 +319,8 @@ NodeChangeListener [] listeners; Preferences childNode; - synchronized (tree.treeLock()) { + synchronized (this) { String childName = evt.getChild().name(); - if (removed || removedChildren.contains(childName)) { - return; - } - childNode = children.get(childName); if (childNode != null) { // swap delegates @@ -578,7 +328,9 @@ } else { childNode = node(evt.getChild().name()); } - + if (!noEvents) { + return; + } listeners = nodeListeners.toArray(new NodeChangeListener[nodeListeners.size()]); } @@ -595,21 +347,18 @@ NodeChangeListener [] listeners; Preferences childNode; - synchronized (tree.treeLock()) { + synchronized (this) { String childName = evt.getChild().name(); - if (removed || removedChildren.contains(childName)) { - return; - } childNode = children.get(childName); - if (childNode != null) { - // swap delegates - ((ProxyPreferences) childNode).changeDelegate(null); - } else { + if (childNode == null) { // nobody has accessed the child yet return; } + if (!noEvents) { + return; + } listeners = nodeListeners.toArray(new NodeChangeListener[nodeListeners.size()]); } @@ -622,445 +371,21 @@ } } - // ------------------------------------------------------------------------ - // Other public implementation - // ------------------------------------------------------------------------ + private final ProxyPreferences parent; + private final Map children = new HashMap(); - /** - * Destroys whole preferences tree as if called on the root. - */ - public void destroy() { - synchronized (tree.treeLock()) { - tree.destroy(); - } - } - - public void silence() { - synchronized (tree.treeLock()) { - noEvents = true; - } - } - - // ------------------------------------------------------------------------ - // private implementation - // ------------------------------------------------------------------------ - - private static final Logger LOG = Logger.getLogger(ProxyPreferences.class.getName()); - - private final ProxyPreferences parent; - private final String name; - private Preferences delegate; - private final Tree tree; - private boolean removed; - - private final Map data = new HashMap(); - private final Set removedKeys = new HashSet(); - private final Map children = new HashMap(); - private final Set removedChildren = new HashSet(); - - private boolean noEvents = false; private PreferenceChangeListener weakPrefListener; private final Set prefListeners = new HashSet(); private NodeChangeListener weakNodeListener; private final Set nodeListeners = new HashSet(); - private ProxyPreferences(ProxyPreferences parent, String name, Preferences delegate, Tree tree) { - assert name != null; - + private ProxyPreferences(ProxyPreferences parent, MemoryPreferences memoryPref, Preferences delegate) { this.parent = parent; - this.name = name; - this.delegate = delegate; - if (delegate != null) { - assert name.equals(delegate.name()); - - weakPrefListener = WeakListeners.create(PreferenceChangeListener.class, this, delegate); - delegate.addPreferenceChangeListener(weakPrefListener); - - weakNodeListener = WeakListeners.create(NodeChangeListener.class, this, delegate); - delegate.addNodeChangeListener(weakNodeListener); - } - this.tree = tree; + this.delegateRoot = memoryPref; + this.delegate = delegate == null ? memoryPref.getPreferences() : delegate; + weakPrefListener = WeakListeners.create(PreferenceChangeListener.class, this, delegate); + this.delegate.addPreferenceChangeListener(weakPrefListener); + weakNodeListener = WeakListeners.create(NodeChangeListener.class, this, delegate); + this.delegate.addNodeChangeListener(weakNodeListener); } - - private void _put(String key, String value, String javaType) { - EventBag bag = null; - - synchronized (tree.treeLock()) { - checkNotNull(key, "key"); //NOI18N - checkNotNull(value, "value"); //NOI18N - checkRemoved(); - - String orig = get(key, null); - if (orig == null || !orig.equals(value)) { - if (LOG.isLoggable(Level.FINE)) { - LOG.fine("Overwriting '" + key + "' = '" + value + "'"); //NOI18N - } - - data.put(key, new TypedValue(value, javaType)); - removedKeys.remove(key); - - bag = new EventBag(); - bag.addListeners(prefListeners); - bag.addEvent(new PreferenceChangeEvent(this, key, value)); - } - } - - if (bag != null) { - firePrefEvents(Collections.singletonList(bag)); - } - } - - private ProxyPreferences node(String pathName, boolean create, List> events) { - if (pathName.length() > 0 && pathName.charAt(0) == '/') { //NOI18N - // absolute path, if this is not the root then find the root - // and pass the call to it - if (parent != null) { - Preferences root = this; - while (root.parent() != null) { - root = root.parent(); - } - return ((ProxyPreferences) root).node(pathName, create, events); - } else { - // this is the root, change the pathName to a relative path and proceed - pathName = pathName.substring(1); - } - } - - if (pathName.length() > 0) { - String childName; - String pathFromChild; - - int idx = pathName.indexOf('/'); //NOI18N - if (idx != -1) { - childName = pathName.substring(0, idx); - pathFromChild = pathName.substring(idx + 1); - } else { - childName = pathName; - pathFromChild = null; - } - - ProxyPreferences child = children.get(childName); - if (child == null) { - if (removedChildren.contains(childName) && !create) { - // this child has been removed - return null; - } - - Preferences childDelegate = null; - try { - if (delegate != null && delegate.nodeExists(childName)) { - childDelegate = delegate.node(childName); - } - } catch (BackingStoreException bse) { - // ignore - } - - if (childDelegate != null || create) { - child = tree.get(this, childName, childDelegate); - children.put(childName, child); - removedChildren.remove(childName); - - // fire event if we really created the new child node - if (childDelegate == null) { - EventBag bag = new EventBag(); - bag.addListeners(nodeListeners); - bag.addEvent(new NodeChangeEventExt(this, child, false)); - events.add(bag); - } - } else { - // childDelegate == null && !create - return null; - } - } else { - assert !child.removed; - } - - return pathFromChild != null ? child.node(pathFromChild, create, events) : child; - } else { - return this; - } - } - - private void addChild(ProxyPreferences child) { - ProxyPreferences pp = children.get(child.name()); - if (pp == null) { - children.put(child.name(), child); - } else { - assert pp == child; - } - } - - private void removeChild(ProxyPreferences child) { - assert child != null; - assert children.get(child.name()) == child; - - child.nodeRemoved(); - children.remove(child.name()); - removedChildren.add(child.name()); - } - - private void nodeRemoved() { - for(ProxyPreferences pp : children.values()) { - pp.nodeRemoved(); - } - - data.clear(); - removedKeys.clear(); - children.clear(); - removedChildren.clear(); - tree.removeNode(this); - - removed = true; - } - - private void checkNotNull(Object paramValue, String paramName) { - if (paramValue == null) { - throw new NullPointerException("The " + paramName + " must not be null"); - } - } - - private void checkRemoved() { - if (removed) { - throw new IllegalStateException("The node '" + this + " has already been removed."); //NOI18N - } - } - - private void changeDelegate(Preferences nueDelegate) { - if (delegate != null) { - try { - if (delegate.nodeExists("")) { //NOI18N - assert weakPrefListener != null; - assert weakNodeListener != null; - delegate.removePreferenceChangeListener(weakPrefListener); - delegate.removeNodeChangeListener(weakNodeListener); - } - } catch (BackingStoreException bse) { - LOG.log(Level.WARNING, null, bse); - } - } - - delegate = nueDelegate; - weakPrefListener = null; - weakNodeListener = null; - - if (delegate != null) { - weakPrefListener = WeakListeners.create(PreferenceChangeListener.class, this, delegate); - delegate.addPreferenceChangeListener(weakPrefListener); - - weakNodeListener = WeakListeners.create(NodeChangeListener.class, this, delegate); - delegate.addNodeChangeListener(weakNodeListener); - } - } - - private void _sync( - List> prefEvents, - List> nodeEvents - ) { - // synchronize all children firts - for(ProxyPreferences pp : children.values()) { - pp._sync(prefEvents, nodeEvents); - } - - // report all new children as removed - EventBag nodeBag = new EventBag(); - nodeBag.addListeners(nodeListeners); - - for(ProxyPreferences pp : children.values()) { - if (pp.delegate == null) { - // new node that does not have corresponding node in the original hierarchy - nodeBag.addEvent(new NodeChangeEventExt(this, pp, true)); - } - } - - if (!nodeBag.getEvents().isEmpty()) { - nodeEvents.add(nodeBag); - } - - // report all modified keys - if (delegate != null) { - EventBag prefBag = new EventBag(); - prefBag.addListeners(prefListeners); - prefEvents.add(prefBag); - - for(String key : data.keySet()) { - prefBag.addEvent(new PreferenceChangeEvent(this, key, delegate.get(key, data.get(key).getValue()))); - } - } // else there is no corresponding node in the orig hierarchy and this node - // will be reported as removed - - // erase modified data - for(NodeChangeEvent nce : nodeBag.getEvents()) { - children.remove(nce.getChild().name()); - } - data.clear(); - } - - private void firePrefEvents(List> events) { - if (noEvents) { - return; - } - - for(EventBag bag : events) { - for(PreferenceChangeEvent event : bag.getEvents()) { - for(PreferenceChangeListener l : bag.getListeners()) { - try { - l.preferenceChange(event); - } catch (Throwable t) { - LOG.log(Level.WARNING, null, t); - } - } - } - } - } - - private void fireNodeEvents(List> events) { - if (noEvents) { - return; - } - - for(EventBag bag : events) { - for(NodeChangeEvent event : bag.getEvents()) { - for(NodeChangeListener l : bag.getListeners()) { - try { - if ((event instanceof NodeChangeEventExt) && ((NodeChangeEventExt) event).isRemovalEvent()) { - l.childRemoved(event); - } else { - l.childAdded(event); - } - } catch (Throwable t) { - LOG.log(Level.WARNING, null, t); - } - } - } - } - } - - /* test */ static final class Tree { - - public static Tree getTree(Object token, Preferences prefs) { - synchronized (trees) { - // find all trees for the token - Map forest = trees.get(token); - if (forest == null) { - forest = new HashMap(); - trees.put(token, forest); - } - - // find the tree for the prefs' root - Preferences root = prefs.node("/"); //NOI18N - Tree tree = forest.get(root); - if (tree == null) { - tree = new Tree(token, root); - forest.put(root, tree); - } - - return tree; - } - } - - /* test */ static final Map> trees = new WeakHashMap>(); - - private final Preferences root; - private final Reference tokenRef; - private final Map nodes = new HashMap(); - - private Tree(Object token, Preferences root) { - this.root = root; - this.tokenRef = new WeakReference(token); - } - - public Object treeLock() { - return this; - } - - public ProxyPreferences get(ProxyPreferences parent, String name, Preferences delegate) { - if (delegate != null) { - assert name.equals(delegate.name()); - - if (parent == null) { - Preferences parentDelegate = delegate.parent(); - if (parentDelegate != null) { - parent = get(null, parentDelegate.name(), parentDelegate); - } // else delegate is the root - } else { - // sanity check - assert parent.delegate == delegate.parent(); - } - } - - String absolutePath; - if (parent == null) { - absolutePath = "/"; //NOI18N - } else if (parent.parent() == null) { - absolutePath = "/" + name; //NOI18N - } else { - absolutePath = parent.absolutePath() + "/" + name; //NOI18N - } - - ProxyPreferences node = nodes.get(absolutePath); - if (node == null) { - node = new ProxyPreferences(parent, name, delegate, this); - nodes.put(absolutePath, node); - - if (parent != null) { - parent.addChild(node); - } - } else { - assert !node.removed; - } - - return node; - } - - public void removeNode(ProxyPreferences node) { - String path = node.absolutePath(); - assert nodes.containsKey(path); - ProxyPreferences pp = nodes.remove(path); - } - - public void destroy() { - synchronized (trees) { - Object token = tokenRef.get(); - if (token != null) { - trees.remove(token); - } // else the token has been GCed and therefore is not even in the trees map - } - } - } // End of Tree class - - private static final class EventBag { - private final Set listeners = new HashSet(); - private final Set events = new HashSet(); - - public EventBag() { - } - - public Set getListeners() { - return listeners; - } - - public Set getEvents() { - return events; - } - - public void addListeners(Collection l) { - listeners.addAll(l); - } - - public void addEvent(E event) { - events.add(event); - } - } // End of EventBag class - - private static final class NodeChangeEventExt extends NodeChangeEvent { - private final boolean removal; - public NodeChangeEventExt(Preferences parent, Preferences child, boolean removal) { - super(parent, child); - this.removal = removal; - } - - public boolean isRemovalEvent() { - return removal; - } - } // End of NodeChangeEventExt class } diff --git a/options.editor/test/unit/src/org/netbeans/modules/options/indentation/ProxyPreferencesTest.java b/options.editor/test/unit/src/org/netbeans/modules/options/indentation/ProxyPreferencesTest.java --- a/options.editor/test/unit/src/org/netbeans/modules/options/indentation/ProxyPreferencesTest.java +++ b/options.editor/test/unit/src/org/netbeans/modules/options/indentation/ProxyPreferencesTest.java @@ -305,7 +305,8 @@ test = null; assertGC("Tree token was not GCed", treeTokenRef, Collections.singleton(this)); // touch the WeakHashMap to expungeStaleEntries - ProxyPreferences.Tree.trees.size(); + Object dummyToken = new Object(); + ProxyPreferences dummyPrefs = ProxyPreferences.getProxyPreferences(dummyToken, orig); assertGC("Test preferences were not GCed", testRef, Collections.singleton(this)); } diff --git a/php.editor/nbproject/project.xml b/php.editor/nbproject/project.xml --- a/php.editor/nbproject/project.xml +++ b/php.editor/nbproject/project.xml @@ -113,6 +113,15 @@ + org.netbeans.modules.editor.fold + + + + 1 + 1.34 + + + org.netbeans.modules.editor.indent diff --git a/php.editor/src/org/netbeans/modules/php/editor/nav/FoldingScanner.java b/php.editor/src/org/netbeans/modules/php/editor/nav/FoldingScanner.java --- a/php.editor/src/org/netbeans/modules/php/editor/nav/FoldingScanner.java +++ b/php.editor/src/org/netbeans/modules/php/editor/nav/FoldingScanner.java @@ -47,6 +47,8 @@ import java.util.List; import java.util.Map; import javax.swing.text.Document; +import org.netbeans.api.editor.fold.FoldTemplate; +import org.netbeans.api.editor.fold.FoldType; import org.netbeans.modules.csl.api.OffsetRange; import org.netbeans.modules.csl.spi.ParserResult; import org.netbeans.modules.parsing.api.Source; @@ -77,17 +79,44 @@ import org.netbeans.modules.php.editor.parser.astnodes.WhileStatement; import org.netbeans.modules.php.editor.parser.astnodes.visitors.DefaultVisitor; +import org.openide.util.NbBundle; +import static org.netbeans.modules.php.editor.nav.Bundle.*; /** * * @author Ondrej Brejla */ public final class FoldingScanner { + + public static final FoldType TYPE_CODE_BLOCKS = FoldType.CODE_BLOCK; + + /** + * FoldType for the PHP class (either nested, or top-level) + */ + @NbBundle.Messages("FT_Classes=Classes") + public static final FoldType TYPE_CLASS = FoldType.NESTED.derive( + "class", + FT_Classes(), FoldTemplate.DEFAULT_BLOCK); - private static final String FOLD_CODE_BLOCKS = "codeblocks"; //NOI18N - private static final String FOLD_CLASS = "inner-classes"; //NOI18N - private static final String FOLD_PHPDOC = "comments"; //NOI18N - private static final String FOLD_COMMENT = "initial-comment"; //NOI18N - private static final String FOLD_OTHER_CODE_BLOCKS = "othercodeblocks"; //NOI18N + /** + * PHP documentation comments + */ + @NbBundle.Messages("FT_PHPDoc=PHPDoc documentation") + public static final FoldType TYPE_PHPDOC = FoldType.DOCUMENTATION.override( + FT_PHPDoc(), // NOI18N + new FoldTemplate(3, 2, "/**...*/")); // NOI18N + + public static final FoldType TYPE_COMMENT = FoldType.COMMENT.override( + FoldType.COMMENT.getLabel(), + new FoldTemplate(2, 2, "/*...*/")); // NOI18N + + /** + * + */ + @NbBundle.Messages("FT_Functions=Functions and methods") + public static final FoldType TYPE_FUNCTION = FoldType.MEMBER.derive("function", + FT_Functions(), + FoldTemplate.DEFAULT_BLOCK); + private static final String LAST_CORRECT_FOLDING_PROPERTY = "LAST_CORRECT_FOLDING_PROPERY"; //NOI18N public static FoldingScanner create() { @@ -120,10 +149,10 @@ if (comments != null) { for (Comment comment : comments) { if (comment.getCommentType() == Comment.Type.TYPE_PHPDOC) { - getRanges(folds, FOLD_PHPDOC).add(createOffsetRange(comment)); + getRanges(folds, TYPE_PHPDOC).add(createOffsetRange(comment, -3)); } else { if (comment.getCommentType() == Comment.Type.TYPE_MULTILINE) { - getRanges(folds, FOLD_COMMENT).add(createOffsetRange(comment)); + getRanges(folds, TYPE_COMMENT).add(createOffsetRange(comment)); } } } @@ -138,10 +167,10 @@ continue; } if (scope instanceof TypeScope) { - getRanges(folds, FOLD_CLASS).add(offsetRange); + getRanges(folds, TYPE_CLASS).add(offsetRange); } else { if (scope instanceof FunctionScope || scope instanceof MethodScope) { - getRanges(folds, FOLD_CODE_BLOCKS).add(offsetRange); + getRanges(folds, TYPE_FUNCTION).add(offsetRange); } } } @@ -157,16 +186,20 @@ } return Collections.emptyMap(); } + + private OffsetRange createOffsetRange(ASTNode node, int startShift) { + return new OffsetRange(node.getStartOffset() + startShift, node.getEndOffset()); + } private OffsetRange createOffsetRange(ASTNode node) { - return new OffsetRange(node.getStartOffset(), node.getEndOffset()); + return createOffsetRange(node, 0); } - private List getRanges(Map> folds, String kind) { - List ranges = folds.get(kind); + private List getRanges(Map> folds, FoldType kind) { + List ranges = folds.get(kind.code()); if (ranges == null) { ranges = new ArrayList(); - folds.put(kind, ranges); + folds.put(kind.code(), ranges); } return ranges; } @@ -290,7 +323,7 @@ } private void addFold(final OffsetRange offsetRange) { - getRanges(folds, FOLD_OTHER_CODE_BLOCKS).add(offsetRange); + getRanges(folds, TYPE_CODE_BLOCKS).add(offsetRange); } } diff --git a/php.editor/src/org/netbeans/modules/php/editor/nav/PHPFoldingProvider.java b/php.editor/src/org/netbeans/modules/php/editor/nav/PHPFoldingProvider.java new file mode 100644 --- /dev/null +++ b/php.editor/src/org/netbeans/modules/php/editor/nav/PHPFoldingProvider.java @@ -0,0 +1,75 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2013 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + * + * Contributor(s): + * + * Portions Copyrighted 2013 Sun Microsystems, Inc. + */ +package org.netbeans.modules.php.editor.nav; + +import java.util.ArrayList; +import java.util.Collection; +import org.netbeans.api.editor.fold.FoldType; +import org.netbeans.api.editor.mimelookup.MimeRegistration; +import org.netbeans.spi.editor.fold.FoldTypeProvider; + +/** + * + * @author sdedic + */ +@MimeRegistration(mimeType = "text/x-php5", service = FoldTypeProvider.class) +public class PHPFoldingProvider implements FoldTypeProvider { + private static final Collection TYPES = new ArrayList(5); + + static { + TYPES.add(FoldingScanner.TYPE_CLASS); + TYPES.add(FoldingScanner.TYPE_FUNCTION); + TYPES.add(FoldingScanner.TYPE_CODE_BLOCKS); + TYPES.add(FoldingScanner.TYPE_COMMENT); + TYPES.add(FoldingScanner.TYPE_PHPDOC); + } + + @Override + public Collection getValues(Class type) { + return type == FoldType.class ? TYPES : null; + } + + @Override + public boolean inheritable() { + return false; + } +} diff --git a/php.editor/src/org/netbeans/modules/php/editor/resources/layer.xml b/php.editor/src/org/netbeans/modules/php/editor/resources/layer.xml --- a/php.editor/src/org/netbeans/modules/php/editor/resources/layer.xml +++ b/php.editor/src/org/netbeans/modules/php/editor/resources/layer.xml @@ -116,7 +116,17 @@ - + + + + + + + + + + +