# HG changeset patch # Parent ffdcf92db51a8207de0560aab9bfcd9d40b82919 diff --git a/editor.fold/apichanges.xml b/editor.fold/apichanges.xml --- a/editor.fold/apichanges.xml +++ b/editor.fold/apichanges.xml @@ -112,6 +112,22 @@ + + + Fold separation from Editor Library + + + + + +

The CodeFoldingSideBar and CustomFoldManager were moved from the editor.lib module + into editor.fold. The classes were deprecated. Factory methods were provided instead, in FoldingSupport +

+
+ + + +
FoldOperation.owns(Fold) added diff --git a/editor.fold/arch.xml b/editor.fold/arch.xml --- a/editor.fold/arch.xml +++ b/editor.fold/arch.xml @@ -787,7 +787,10 @@ --> -No. + + Injects its handler to be used with BaseCaret as client property of the JTextComponent that initializes + folding. + 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.37 + + + org.netbeans.modules.editor.lib2 @@ -85,6 +94,15 @@ + org.netbeans.modules.lexer + + + + 2 + 1.50 + + + org.openide.filesystems @@ -143,10 +161,27 @@ + + org.netbeans.modules.editor + + + + + + org.netbeans.modules.editor.settings.storage + + + + + org.netbeans.modules.editor.mimelookup.impl + + + org.netbeans.api.editor.fold + org.netbeans.editor org.netbeans.spi.editor.fold 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,142 @@ +/* + * 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.ui.CodeFoldingSideBar; +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 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; 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/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,6 +57,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; import java.util.prefs.Preferences; @@ -174,6 +175,8 @@ private Task initTask; + private FoldingEditorSupport editSupport; + public static synchronized FoldHierarchy getOrCreateFoldHierarchy(JTextComponent component) { return getOrCreateFoldExecution(component).getHierarchy(); } @@ -256,6 +259,9 @@ } catch (BadLocationException e) { ErrorManager.getDefault().notify(e); } + + editSupport = new FoldingEditorSupport(hierarchy, component); + } /* testing only */ @@ -263,6 +269,11 @@ getOrCreateFoldExecution(panel).getInitTask().waitFinished(); } + /* testing only */ + static boolean waitAllTasks() throws InterruptedException { + return RP.awaitTermination(30, TimeUnit.SECONDS); + } + private Task getInitTask() { return initTask; } 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/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,172 @@ +/* + * 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 to expand folds at the caret position, + * if possible. + * + * @return true, if a fold was expanded, true otherwise. + */ + 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/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,1507 @@ +/* + * 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 void updatePreferredSize() { + // do not show at all, if there are no providers registered. + 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() { + 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/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/FoldView.java b/editor.fold/src/org/netbeans/modules/editor/fold/ui/FoldView.java rename from editor.fold/src/org/netbeans/modules/editor/fold/FoldView.java rename to editor.fold/src/org/netbeans/modules/editor/fold/ui/FoldView.java --- a/editor.fold/src/org/netbeans/modules/editor/fold/FoldView.java +++ b/editor.fold/src/org/netbeans/modules/editor/fold/ui/FoldView.java @@ -42,7 +42,7 @@ * made subject to such option by the copyright holder. */ -package org.netbeans.modules.editor.fold; +package org.netbeans.modules.editor.fold.ui; import java.awt.Color; import java.awt.Container; diff --git a/editor.fold/src/org/netbeans/modules/editor/fold/FoldViewFactory.java b/editor.fold/src/org/netbeans/modules/editor/fold/ui/FoldViewFactory.java rename from editor.fold/src/org/netbeans/modules/editor/fold/FoldViewFactory.java rename to editor.fold/src/org/netbeans/modules/editor/fold/ui/FoldViewFactory.java --- a/editor.fold/src/org/netbeans/modules/editor/fold/FoldViewFactory.java +++ b/editor.fold/src/org/netbeans/modules/editor/fold/ui/FoldViewFactory.java @@ -42,7 +42,7 @@ * made subject to such option by the copyright holder. */ -package org.netbeans.modules.editor.fold; +package org.netbeans.modules.editor.fold.ui; import java.util.Iterator; import java.util.logging.Level; @@ -85,7 +85,7 @@ // -J-Dorg.netbeans.modules.editor.fold.FoldViewFactory.level=FINE private static final Logger LOG = Logger.getLogger(FoldViewFactory.class.getName()); - static void register() { + public static void register() { EditorViewFactory.registerFactory(new FoldFactory()); } diff --git a/editor.fold/test/unit/src/org/netbeans/modules/editor/fold/BaseCaretTest.java b/editor.fold/test/unit/src/org/netbeans/modules/editor/fold/BaseCaretTest.java new file mode 100644 --- /dev/null +++ b/editor.fold/test/unit/src/org/netbeans/modules/editor/fold/BaseCaretTest.java @@ -0,0 +1,374 @@ +/* + * 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.AWTEvent; +import java.awt.Component; +import java.awt.Rectangle; +import java.awt.Window; +import java.awt.event.InputEvent; +import java.awt.event.MouseEvent; +import java.beans.PropertyVetoException; +import java.io.IOException; +import java.lang.reflect.Method; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.prefs.AbstractPreferences; +import java.util.prefs.BackingStoreException; +import javax.swing.JEditorPane; +import javax.swing.JFrame; +import javax.swing.SwingUtilities; +import javax.swing.event.DocumentEvent; +import javax.swing.text.BadLocationException; +import org.netbeans.api.editor.fold.Fold; +import org.netbeans.api.editor.fold.FoldType; +import org.netbeans.api.editor.fold.FoldUtilities; +import org.netbeans.editor.BaseCaret; +import org.netbeans.editor.BaseKit; +import org.netbeans.editor.ext.ExtKit; +import org.netbeans.junit.NbTestCase; +import org.netbeans.modules.editor.NbEditorKit; +//import org.netbeans.modules.editor.NbEditorKit; +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.filesystems.Repository; +import org.openide.filesystems.XMLFileSystem; +import org.openide.util.Exceptions; +import org.openide.util.Lookup; +import org.openide.util.lookup.Lookups; +import org.openide.util.lookup.ProxyLookup; +import org.xml.sax.SAXException; + +import static junit.framework.Assert.assertEquals; + +/** + * Checks that the folding succeeds in plugging into BaseCaret's operation. + * It checks that a doubleclick on a folded place does not select the surrounding text. + * Likewise it checks, that a doubleclick on an unfolded place WILL select the surrounding text + * + * @author sdedic + */ +public class BaseCaretTest extends NbTestCase implements Callable { + private JEditorPane pane; + private FoldHierarchyTestEnv env; + public static Lkp DEFAULT_LOOKUP = null; + + public BaseCaretTest(String name) { + super(name); + } + + protected boolean runInEQ() { + return true; + } + + public static void setLookup(Object[] instances, ClassLoader cl) { + DEFAULT_LOOKUP.doSetLookups(new Lookup[] { + Lookups.fixed(instances), + Lookups.metaInfServices(cl), + Lookups.singleton(cl), + }); + } + + public static class Lkp extends ProxyLookup { + { + DEFAULT_LOOKUP = this; + } + + public void doSetLookups(Lookup... lkps) { + super.setLookups(lkps); + } + } + + private static final Method PROCESS_EVENT_METHOD; + + static { + System.setProperty("org.openide.util.Lookup", BaseCaretTest.Lkp.class.getName()); + Method m = null; + + try { + m = Component.class.getDeclaredMethod("processEvent", new Class[] { AWTEvent.class }); + m.setAccessible(true); + } catch (Exception ex) { + Exceptions.printStackTrace(ex); + } + PROCESS_EVENT_METHOD = m; + } + + public static void prepareTest(String[] additionalLayers, Object[] additionalLookupContent) throws IOException, SAXException, PropertyVetoException { + Collection allUrls = new ArrayList(); + for (String u : additionalLayers) { + if (u.charAt(0) == '/') { + u = u.substring(1); + } + for (Enumeration en = Thread.currentThread().getContextClassLoader().getResources(u); en.hasMoreElements(); ) { + allUrls.add(en.nextElement()); + } + } + XMLFileSystem system = new XMLFileSystem(); + system.setXmlUrls(allUrls.toArray(new URL[allUrls.size()])); + + Repository repository = new Repository(system); + Object[] lookupContent = new Object[additionalLookupContent.length + 1]; + + System.arraycopy(additionalLookupContent, 0, lookupContent, 1, additionalLookupContent.length); + + lookupContent[0] = repository; + + setLookup(lookupContent, BaseCaretTest.class.getClassLoader()); + } + + private FM fm; + + private Fold fold; + + private Callable delegate; + + public void setUp() throws Exception { + super.setUp(); + Lookup.getDefault(); + env = new FoldHierarchyTestEnv(new FoldManagerFactory() { + @Override + public FoldManager createFoldManager() { + return fm = new FM(); + } + }); + prepareTest(new String[] { + "/org/netbeans/modules/editor/resources/layer.xml", + "/META-INF/generated-layer.xml", + "/org/netbeans/modules/defaults/mf-layer.xml", + }, + new Object[0]); + pane = env.getPane(); + env.getPane().setEditorKit(new NbEditorKit()); + env.getDocument().insertString(0, "123456789-123456789-123 aa 89-123456789-1234567890", null); + + // ensure initialized + env.getHierarchy(); + // cannot be done in FoldManager, as setEditorKit() will replace document and reinitialize folds in 2nd thread, which may + // complete even faster than insertString(), so BLE could be thrown. + FoldHierarchyTransaction tran = fm.op.openTransaction(); + fm.op.addToHierarchy( + FT, + "", + true, + 10, + 40, + 1, 1, + null, + tran); + tran.commit(); + + fold = FoldUtilities.findOffsetFold(env.getHierarchy(), 25); + // get x/y of the folded part: + JFrame fr = new JFrame("test"); + fr.setBounds(0, 0, 400, 100); + fr.add(pane); + fr.setVisible(true); + + delegate = (Callable)pane.getClientProperty("org.netbeans.api.fold.expander"); + pane.putClientProperty("org.netbeans.api.fold.expander", this); + pane.requestFocus(); + } + + public void tearDown() throws Exception { + Window w = SwingUtilities.getWindowAncestor(pane); + w.setVisible(false); + super.tearDown(); + } + + private void processEvent(AWTEvent e) throws Exception{ + PROCESS_EVENT_METHOD.invoke(pane, e); + } + + private boolean retValue; + + /** + * Intercept the real callable + */ + @Override + public Boolean call() throws Exception { + return retValue = delegate.call(); + } + + /** + * Folded region is double-clicked; it should be unfolded, rather than word-selected. + */ + public void testSelectFoldedRegion() throws Exception { + env.getHierarchy().collapse(fold); + Rectangle r = pane.modelToView(25); + + // 1st mouseclick on the pane, to position the caret, as if the 1st click in doubleclick was done: + MouseEvent firstPress = new MouseEvent(pane, MouseEvent.MOUSE_PRESSED, System.currentTimeMillis(), + InputEvent.BUTTON1_MASK, r.x, r.y, 1, false); + processEvent(firstPress); + + // second press of the doubleclick + MouseEvent secondPress = new MouseEvent(pane, MouseEvent.MOUSE_PRESSED, System.currentTimeMillis(), + InputEvent.BUTTON1_MASK, r.x, r.y, 2, false); + processEvent(secondPress); + + // check that no selection is present: + assertEquals(pane.getSelectionEnd(), pane.getSelectionStart()); + assertNull(pane.getSelectedText()); + // check that even the callable did the job: + assertFalse(fold.isCollapsed()); + } + + public void testSelectUnfoldedRegion() throws Exception { + Rectangle r = pane.modelToView(25); + + // 1st mouseclick on the pane, to position the caret, as if the 1st click in doubleclick was done: + MouseEvent firstPress = new MouseEvent(pane, MouseEvent.MOUSE_PRESSED, System.currentTimeMillis(), + InputEvent.BUTTON1_MASK, r.x, r.y, 1, false); + processEvent(firstPress); + + // second press of the doubleclick + MouseEvent secondPress = new MouseEvent(pane, MouseEvent.MOUSE_PRESSED, System.currentTimeMillis(), + InputEvent.BUTTON1_MASK, r.x, r.y, 2, false); + + processEvent(secondPress); + + int start = pane.getSelectionStart(); + int end = pane.getSelectionEnd(); + assertTrue(start < end); + } + + private static final FoldType FT = new FoldType("test"); + + private static class FM implements FoldManager { + FoldOperation op; + + @Override + public void init(FoldOperation operation) { + this.op = operation; + } + + @Override + public void initFolds(FoldHierarchyTransaction transaction) { + } + + @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() { + } + } + + private static class P extends AbstractPreferences { + private Map m = new HashMap(); + + public P(AbstractPreferences parent, String name) { + super(parent, name); + } + + @Override + protected void putSpi(String key, String value) { + m.put(key, value); + } + + @Override + protected String getSpi(String key) { + return m.get(key); + } + + @Override + protected void removeSpi(String key) { + m.remove(key); + } + + @Override + protected void removeNodeSpi() throws BackingStoreException { + } + + @Override + protected String[] keysSpi() throws BackingStoreException { + return new String[0]; + } + + @Override + protected String[] childrenNamesSpi() throws BackingStoreException { + return new String[0]; + } + + @Override + protected AbstractPreferences childSpi(String name) { + return null; + } + + @Override + protected void syncSpi() throws BackingStoreException { + } + + @Override + protected void flushSpi() throws BackingStoreException { + } + } +} 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