diff --git a/.hgtags b/.hgtags --- a/.hgtags +++ b/.hgtags @@ -962,4 +962,6 @@ 37fc29481ec42daf8347ecc28cdf38079401b033 release81_beta_base ca20d6d4ed09fed2af119a1fd21a2eee31751d9e build_27 294dca090459562fe1bbe59c5a4e61ab95d24d42 release81_base +5cf4b63c4b9823e6fc4a8137827c99b8a753d773 editor_multi_caret_stable 8d2520b6f9672304f6326258c34ed8d2509e6d2a jdk9ea + diff --git a/editor.actions/src/org/netbeans/modules/editor/actions/AddSelectionElseCaretAction.java b/editor.actions/src/org/netbeans/modules/editor/actions/AddSelectionElseCaretAction.java new file mode 100644 --- /dev/null +++ b/editor.actions/src/org/netbeans/modules/editor/actions/AddSelectionElseCaretAction.java @@ -0,0 +1,162 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2016 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 2016 Sun Microsystems, Inc. + */ +package org.netbeans.modules.editor.actions; + +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.event.ActionEvent; +import java.util.ArrayList; +import java.util.List; +import javax.swing.text.BadLocationException; +import javax.swing.text.Caret; +import javax.swing.text.JTextComponent; +import javax.swing.text.Position; +import org.netbeans.api.editor.EditorActionNames; +import org.netbeans.api.editor.EditorActionRegistration; +import org.netbeans.api.editor.EditorActionRegistrations; +import org.netbeans.api.editor.caret.EditorCaret; +import org.netbeans.api.editor.caret.CaretMoveContext; +import org.netbeans.editor.BaseDocument; +import org.netbeans.editor.Utilities; +import org.netbeans.spi.editor.AbstractEditorAction; +import org.netbeans.spi.editor.caret.CaretMoveHandler; + +/** + * + * @author Ralph Ruijs + */ +@EditorActionRegistrations({ + @EditorActionRegistration(name = EditorActionNames.addSelectionElseCaretUpAction), + @EditorActionRegistration(name = EditorActionNames.addSelectionElseCaretDownAction) +}) +public class AddSelectionElseCaretAction extends AbstractEditorAction { + + @Override + protected void actionPerformed(ActionEvent evt, final JTextComponent target) { + if (target != null) { + Caret caret = target.getCaret(); + if (caret != null && caret instanceof EditorCaret) { + final EditorCaret editorCaret = (EditorCaret) caret; + final BaseDocument doc = (BaseDocument) target.getDocument(); + if(EditorActionNames.addSelectionElseCaretUpAction.equals(actionName())) { + doc.runAtomicAsUser(new Runnable() { + @Override + public void run() { + final List dots = new ArrayList<>(editorCaret.getCarets().size() << 1); + editorCaret.moveCarets(new CaretMoveHandler() { + @Override + public void moveCarets(CaretMoveContext context) { + for (org.netbeans.api.editor.caret.CaretInfo caretInfo : context.getOriginalCarets()) { + try { + int dot = caretInfo.getDot(); + Point p = caretInfo.getMagicCaretPosition(); + if (p == null) { + Rectangle r = target.modelToView(dot); + if (r != null) { + p = new Point(r.x, r.y); + context.setMagicCaretPosition(caretInfo, p); + } else { + return; // model to view failed + } + } + try { + dot = Utilities.getPositionAbove(target, dot, p.x); + Position dotPos = doc.createPosition(dot); + dots.add(dotPos); + dots.add(dotPos); + } catch (BadLocationException e) { + // the position stays the same + } + } catch (BadLocationException ex) { + target.getToolkit().beep(); + } + } + } + }); + + editorCaret.addCarets(dots); + } + }); + } else { + doc.runAtomicAsUser(new Runnable() { + @Override + public void run() { + final List dots = new ArrayList<>(editorCaret.getCarets().size() << 1); + editorCaret.moveCarets(new CaretMoveHandler() { + @Override + public void moveCarets(CaretMoveContext context) { + for (org.netbeans.api.editor.caret.CaretInfo caretInfo : context.getOriginalCarets()) { + try { + int dot = caretInfo.getDot(); + Point p = caretInfo.getMagicCaretPosition(); + if (p == null) { + Rectangle r = target.modelToView(dot); + if (r != null) { + p = new Point(r.x, r.y); + context.setMagicCaretPosition(caretInfo, p); + } else { + return; // model to view failed + } + } + try { + dot = Utilities.getPositionBelow(target, dot, p.x); + Position dotPos = doc.createPosition(dot); + dots.add(dotPos); + dots.add(dotPos); + } catch (BadLocationException e) { + // position stays the same + } + } catch (BadLocationException ex) { + target.getToolkit().beep(); + } + } + } + }); + editorCaret.addCarets(dots); + } + }); + } + } + } + } + +} diff --git a/editor.actions/src/org/netbeans/modules/editor/actions/Bundle.properties b/editor.actions/src/org/netbeans/modules/editor/actions/Bundle.properties --- a/editor.actions/src/org/netbeans/modules/editor/actions/Bundle.properties +++ b/editor.actions/src/org/netbeans/modules/editor/actions/Bundle.properties @@ -52,6 +52,7 @@ toggle-line-numbers_menu_text=&Show Line Numbers toggle-non-printable-characters=Toggle Non-printable Characters toggle-non-printable-characters_menu_text=Show &Non-printable Characters +toggle-typing-mode=Toggle Typing Mode transpose-letters=Transpose Letters goto-declaration=Go to Declaration goto-declaration_menu_text=Go to &Declaration @@ -68,4 +69,8 @@ caret-previous-word=Insertion Point to Previous Word selection-next-word=Extend Selection to Next Word selection-previous-word=Extend Selection to Previous Word -toggle-lines-view=Show &Indent Guide Lines \ No newline at end of file +toggle-lines-view=Show &Indent Guide Lines +remove-last-caret=Remove Last Caret +remove-last-caret_menu_text=Remove Last Caret +add-selection-else-caret-up=Duplicate Insertion Point Up +add-selection-else-caret-down=Duplicate Insertion Point Down \ No newline at end of file diff --git a/editor.actions/src/org/netbeans/modules/editor/actions/RemoveLastCaretAction.java b/editor.actions/src/org/netbeans/modules/editor/actions/RemoveLastCaretAction.java new file mode 100644 --- /dev/null +++ b/editor.actions/src/org/netbeans/modules/editor/actions/RemoveLastCaretAction.java @@ -0,0 +1,74 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2016 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 2016 Sun Microsystems, Inc. + */ +package org.netbeans.modules.editor.actions; + +import java.awt.event.ActionEvent; +import javax.swing.text.Caret; +import javax.swing.text.JTextComponent; +import org.netbeans.api.editor.EditorActionNames; +import org.netbeans.api.editor.EditorActionRegistration; +import org.netbeans.api.editor.EditorActionRegistrations; +import org.netbeans.api.editor.caret.EditorCaret; +import org.netbeans.spi.editor.AbstractEditorAction; + +/** + * + * @author Ralph Ruijs + */ +@EditorActionRegistrations({ + @EditorActionRegistration(name = EditorActionNames.removeLastCaret, + menuPath = "Edit", + menuPosition = 840, + menuText = "#" + EditorActionNames.removeLastCaret + "_menu_text") +}) +public class RemoveLastCaretAction extends AbstractEditorAction { + + @Override + protected void actionPerformed(ActionEvent evt, JTextComponent component) { + Caret caret = component.getCaret(); + if(caret instanceof EditorCaret) { + EditorCaret editorCaret = (EditorCaret) caret; + editorCaret.removeLastCaret(); + } + } + +} diff --git a/editor.actions/src/org/netbeans/modules/editor/actions/ToggleTypingModeAction.java b/editor.actions/src/org/netbeans/modules/editor/actions/ToggleTypingModeAction.java new file mode 100644 --- /dev/null +++ b/editor.actions/src/org/netbeans/modules/editor/actions/ToggleTypingModeAction.java @@ -0,0 +1,76 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2015 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 2015 Sun Microsystems, Inc. + */ +package org.netbeans.modules.editor.actions; + +import java.awt.event.ActionEvent; +import javax.swing.text.JTextComponent; +import org.netbeans.api.editor.EditorActionNames; +import org.netbeans.api.editor.EditorActionRegistration; +import org.netbeans.api.editor.EditorUtilities; +import org.netbeans.spi.editor.AbstractEditorAction; + +/** + * Switch caret from insert mode to overwrite mode or vice versa. + * + * @author Miloslav Metelka + */ + +@EditorActionRegistration(name = EditorActionNames.toggleTypingMode) +public final class ToggleTypingModeAction extends AbstractEditorAction { + + private static final long serialVersionUID = 1L; + + public ToggleTypingModeAction() { + super(); + } + + @Override + public void actionPerformed(ActionEvent evt, JTextComponent target) { + if (target != null) { + Boolean overwriteMode = (Boolean) target.getClientProperty(EditorUtilities.CARET_OVERWRITE_MODE_PROPERTY); + // Now toggle + overwriteMode = (overwriteMode == null || !overwriteMode) ? Boolean.TRUE : Boolean.FALSE; + target.putClientProperty(EditorUtilities.CARET_OVERWRITE_MODE_PROPERTY, overwriteMode); + } + } + +} diff --git a/editor.document/apichanges.xml b/editor.document/apichanges.xml --- a/editor.document/apichanges.xml +++ b/editor.document/apichanges.xml @@ -112,6 +112,24 @@ + + + Added ShiftPositions + + + + + +

+ Added ShiftPositions class to create and read ShiftPositions. + The implementation ShiftPos is a position together with a + shift of extra columns. This allows for positions behind + line's last character (newline) or within a tab character. +

+
+ + +
Added TextSearchUtils.getPreviousWordStart() method diff --git a/editor.document/nbproject/project.properties b/editor.document/nbproject/project.properties --- a/editor.document/nbproject/project.properties +++ b/editor.document/nbproject/project.properties @@ -1,6 +1,6 @@ javac.source=1.7 javac.compilerargs=-Xlint -Xlint:-serial -spec.version.base=1.6.0 +spec.version.base=1.7.0 javadoc.arch=${basedir}/arch.xml javadoc.apichanges=${basedir}/apichanges.xml is.autoload=true diff --git a/editor.document/src/org/netbeans/api/editor/document/ShiftPositions.java b/editor.document/src/org/netbeans/api/editor/document/ShiftPositions.java new file mode 100644 --- /dev/null +++ b/editor.document/src/org/netbeans/api/editor/document/ShiftPositions.java @@ -0,0 +1,135 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2015 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 2015 Sun Microsystems, Inc. + */ +package org.netbeans.api.editor.document; + +import javax.swing.text.Position; +import org.netbeans.api.annotations.common.NonNull; +import org.netbeans.modules.editor.lib2.document.ShiftPos; +import org.openide.util.Parameters; + +/** + * A position together with a shift of extra columns. + * This allows for positions behind line's last character (newline) or within a tab character. + * + * @author Miloslav Metelka + * @since 1.7 + */ +public final class ShiftPositions { + + private ShiftPositions() { + // No instances + } + + /** + * Produce an immutable shift position. + * The returned position acts like the original position and it does not handle in any way + * any subsequent document modifications. + * + * @param pos non-null position. If this is already a shift position its shift + * gets added to the shift parameter passed to this method. + * @param shift >= 0 number of extra columns added to the position. + * For 0 the original position gets returned. Negative value throws an IllegalArgumentException. + * @return virtual position whose {@link Position#getOffset()} returns the same value + * like the getPosition parameter. + */ + public static Position create(@NonNull Position pos, int shift) { + Parameters.notNull("pos", pos); //NOI18N + if (shift > 0) { + if (pos.getClass() == ShiftPos.class) { + return new ShiftPos((ShiftPos)pos, shift); + } else { + return new ShiftPos(pos, shift); + } + } else if (shift == 0) { + return pos; + } else { + throw new IllegalArgumentException("shift=" + shift + " < 0"); + } + } + + /** + * Return shift of a passed virtual position or zero for regular positions. + * @param pos non-null position. + * @return >=0 shift or zero for regular positions. + */ + public static int getShift(@NonNull Position pos) { + return getShiftImpl(pos); + } + + /** + * Compare positions. + * @param pos1 non-null position. + * @param pos2 non-null position. + * @return offset of pos1 minus offset of pos2 or diff of their shifts in case + * both positions have the same offset. + * @NullPointerException if any passed position is null unless both positions are null + * in which case the method would return 0. + */ + public static int compare(@NonNull Position pos1, @NonNull Position pos2) { + if (pos1 == pos2) { + return 0; + } + int offsetDiff = pos1.getOffset() - pos2.getOffset(); + return (offsetDiff != 0) ? offsetDiff : getShiftImpl(pos1) - getShiftImpl(pos2); + } + + /** + * Compare positions by providing their offsets and shifts obtained earlier. + * @param offset1 offset of first position. + * @param shift1 shift of first position. + * @param offset2 offset of second position. + * @param shift2 shift of second position. + * @return offset1 minus offset2 or shift1 minus shift2 in case + * offset1 and offset2 are equal. + */ + public static int compare(int offset1, int shift1, int offset2, int shift2) { + int offsetDiff = offset1 - offset2; + return (offsetDiff != 0) ? offsetDiff : shift1 - shift2; + } + + private static int getShiftImpl(Position pos) { + return (pos.getClass() == ShiftPos.class) + ? ((ShiftPos)pos).getShift() + : 0; + } + +} diff --git a/editor.document/src/org/netbeans/modules/editor/lib2/document/ShiftPos.java b/editor.document/src/org/netbeans/modules/editor/lib2/document/ShiftPos.java new file mode 100644 --- /dev/null +++ b/editor.document/src/org/netbeans/modules/editor/lib2/document/ShiftPos.java @@ -0,0 +1,83 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2015 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 2015 Sun Microsystems, Inc. + */ +package org.netbeans.modules.editor.lib2.document; + +import javax.swing.text.Position; + + +/** + * Implementation of a shift position. The clients should only use methods + * in {@link org.netbeans.api.editor.document.ShiftPositions} and never check + * for an instance of this particular class. + * + * @author Miloslav Metelka + */ +public final class ShiftPos implements Position { + + private final Position pos; + + private final int shift; + + public ShiftPos(Position pos, int shift) { + this.pos = pos; + this.shift = shift; + } + + public ShiftPos(ShiftPos shiftPos, int shift) { + this.pos = shiftPos.pos; + this.shift = shiftPos.shift + shift; + } + + public Position getPosition() { + return pos; + } + + public int getShift() { + return shift; + } + + @Override + public int getOffset() { + return pos.getOffset(); + } + +} diff --git a/editor.document/test/unit/src/org/netbeans/api/editor/document/ShiftPositionsTest.java b/editor.document/test/unit/src/org/netbeans/api/editor/document/ShiftPositionsTest.java new file mode 100644 --- /dev/null +++ b/editor.document/test/unit/src/org/netbeans/api/editor/document/ShiftPositionsTest.java @@ -0,0 +1,100 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2016 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 2016 Sun Microsystems, Inc. + */ +package org.netbeans.api.editor.document; + +import javax.swing.text.Document; +import javax.swing.text.PlainDocument; +import javax.swing.text.Position; +import org.junit.Test; +import static org.junit.Assert.*; + +/** + * + * @author Miloslav Metelka + */ +public class ShiftPositionsTest { + + public ShiftPositionsTest() { + } + + @Test + public void testPos() throws Exception { + Document doc = new PlainDocument(); + doc.insertString(0, "\t\t\n\n", null); + Position pos1 = doc.createPosition(1); + Position pos2 = doc.createPosition(2); + + Position pos10 = ShiftPositions.create(pos1, 0); + Position pos11 = ShiftPositions.create(pos1, 1); + Position pos20 = ShiftPositions.create(pos2, 0); + Position pos21 = ShiftPositions.create(pos2, 1); + + assertEquals(0, ShiftPositions.getShift(pos1)); + assertEquals(0, ShiftPositions.getShift(pos10)); + assertEquals(1, ShiftPositions.getShift(pos11)); + comparePos(pos1, pos10, 0); + comparePos(pos10, pos11, -1); + comparePos(pos1, pos2, -1); + comparePos(pos10, pos20, -1); + comparePos(pos20, pos21, -1); + } + + private void comparePos(Position pos1, Position pos2, int expectedResult) { + comparePosImpl(pos1, pos2, expectedResult, true); + } + + private void comparePosImpl(Position pos1, Position pos2, int expectedResult, boolean reverseCompare) { + int result = ShiftPositions.compare(pos1, pos2); + assertEquals("Invalid result=" + result + " when comparing positions pos1=" + + pos1 + " to pos2=" + pos2, expectedResult, result); + + result = ShiftPositions.compare(pos1.getOffset(), ShiftPositions.getShift(pos1), + pos2.getOffset(), ShiftPositions.getShift(pos2)); + assertEquals("Invalid result=" + result + " when comparing positions pos1=" + + pos1 + " to pos2=" + pos2, expectedResult, result); + + if (reverseCompare) { + comparePosImpl(pos2, pos1, -expectedResult, false); + } + } + +} diff --git a/editor.errorstripe/nbproject/project.xml b/editor.errorstripe/nbproject/project.xml --- a/editor.errorstripe/nbproject/project.xml +++ b/editor.errorstripe/nbproject/project.xml @@ -84,6 +84,15 @@ + org.netbeans.modules.editor.lib2 + + + + 1 + 2.5 + + + org.netbeans.modules.editor.mimelookup diff --git a/editor.errorstripe/src/org/netbeans/modules/editor/errorstripe/caret/CaretMarkProvider.java b/editor.errorstripe/src/org/netbeans/modules/editor/errorstripe/caret/CaretMarkProvider.java --- a/editor.errorstripe/src/org/netbeans/modules/editor/errorstripe/caret/CaretMarkProvider.java +++ b/editor.errorstripe/src/org/netbeans/modules/editor/errorstripe/caret/CaretMarkProvider.java @@ -44,12 +44,16 @@ package org.netbeans.modules.editor.errorstripe.caret; import java.util.Collections; +import java.util.LinkedList; import java.util.List; import javax.swing.event.CaretEvent; import javax.swing.event.CaretListener; +import javax.swing.text.Caret; import javax.swing.text.Document; import javax.swing.text.JTextComponent; import javax.swing.text.StyledDocument; +import org.netbeans.api.editor.caret.CaretInfo; +import org.netbeans.api.editor.caret.EditorCaret; import org.netbeans.modules.editor.errorstripe.privatespi.Mark; import org.netbeans.modules.editor.errorstripe.privatespi.MarkProvider; import org.openide.text.NbDocument; @@ -64,42 +68,55 @@ private static final RequestProcessor RP = new RequestProcessor("CaretMarkProvider"); - private Mark mark; + private List marks; private JTextComponent component; /** Creates a new instance of AnnotationMarkProvider */ public CaretMarkProvider(JTextComponent component) { this.component = component; component.addCaretListener(this); - mark = createMark(); + marks = createMarks(); } - private Mark createMark() { - int offset = component.getCaretPosition(); //TODO: AWT? + private List createMarks() { Document doc = component.getDocument(); - int line = 0; - - if (doc instanceof StyledDocument) { - line = NbDocument.findLineNumber((StyledDocument) doc, offset); + if(!(doc instanceof StyledDocument)) { + return Collections.singletonList((Mark)new CaretMark(0)); } - - return new CaretMark(line); + List lines = new LinkedList<>(); + Caret caret = component.getCaret(); + if(caret instanceof EditorCaret) { + EditorCaret editorCaret = (EditorCaret) caret; + for (CaretInfo caretInfo : editorCaret.getCarets()) { + int offset = caretInfo.getDot(); + int line = NbDocument.findLineNumber((StyledDocument) doc, offset); + lines.add(new CaretMark(line)); + } + } else { + int offset = component.getCaretPosition(); //TODO: AWT? + int line = NbDocument.findLineNumber((StyledDocument) doc, offset); + lines.add(new CaretMark(line)); + } + return lines; } + @Override public synchronized List getMarks() { - return Collections.singletonList(mark); + return Collections.unmodifiableList(marks); } + @Override public void caretUpdate(CaretEvent e) { final List old = getMarks(); - mark = createMark(); + marks = createMarks(); final List nue = getMarks(); //Do not fire this event under the document's write lock //may deadlock with other providers: RP.post(new Runnable() { + @Override public void run() { firePropertyChange(PROP_MARKS, old, nue); } 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 @@ -46,6 +46,7 @@ import java.awt.Component; import java.awt.Cursor; +import java.awt.Point; import java.awt.Rectangle; import java.awt.Toolkit; import java.awt.event.ActionEvent; @@ -56,6 +57,7 @@ import java.lang.ref.Reference; import java.lang.ref.WeakReference; import java.text.SimpleDateFormat; +import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.HashSet; @@ -87,9 +89,12 @@ import javax.swing.text.AbstractDocument; import javax.swing.text.View; import javax.swing.undo.UndoManager; +import org.netbeans.api.editor.caret.CaretInfo; import org.netbeans.api.editor.EditorActionNames; import org.netbeans.api.editor.EditorActionRegistration; import org.netbeans.api.editor.EditorActionRegistrations; +import org.netbeans.api.editor.EditorUtilities; +import org.netbeans.api.editor.caret.EditorCaret; import org.netbeans.api.editor.fold.FoldHierarchy; import org.netbeans.api.lexer.TokenHierarchy; import org.netbeans.api.progress.ProgressUtils; @@ -98,6 +103,8 @@ import org.netbeans.modules.editor.indent.api.Indent; import org.netbeans.modules.editor.indent.api.Reformat; import org.netbeans.api.editor.NavigationHistory; +import org.netbeans.api.editor.caret.CaretMoveContext; +import org.netbeans.spi.editor.caret.CaretMoveHandler; import org.netbeans.modules.editor.lib2.RectangularSelectionUtils; import org.netbeans.modules.editor.lib2.view.DocumentView; import org.netbeans.spi.editor.typinghooks.CamelCaseInterceptor; @@ -105,6 +112,7 @@ import org.openide.util.ImageUtilities; import org.openide.util.Lookup; import org.openide.util.NbBundle; +import org.openide.util.Pair; import org.openide.util.actions.Presenter; /** @@ -854,8 +862,9 @@ } } - /** Switch to overwrite mode or back to insert mode */ - @EditorActionRegistration(name = BaseKit.toggleTypingModeAction) + /** Switch to overwrite mode or back to insert mode + * @deprecated Replaced by ToggleTypingModeAction in editor.actions module + */ public static class ToggleTypingModeAction extends LocalBaseAction { static final long serialVersionUID =-2431132686507799723L; @@ -866,12 +875,11 @@ public void actionPerformed(ActionEvent evt, JTextComponent target) { if (target != null) { - EditorUI editorUI = Utilities.getEditorUI(target); - Boolean overwriteMode = (Boolean)editorUI.getProperty(EditorUI.OVERWRITE_MODE_PROPERTY); + Boolean overwriteMode = (Boolean) target.getClientProperty(EditorUtilities.CARET_OVERWRITE_MODE_PROPERTY); // Now toggle overwriteMode = (overwriteMode == null || !overwriteMode.booleanValue()) ? Boolean.TRUE : Boolean.FALSE; - editorUI.putProperty(EditorUI.OVERWRITE_MODE_PROPERTY, overwriteMode); + target.putClientProperty(EditorUtilities.CARET_OVERWRITE_MODE_PROPERTY, overwriteMode); } } } 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 @@ -137,14 +137,14 @@ /** Caret type representing block covering current character */ public static final String BLOCK_CARET = EditorPreferencesDefaults.BLOCK_CARET; // NOI18N - /** Default caret type */ - public static final String LINE_CARET = EditorPreferencesDefaults.LINE_CARET; // NOI18N - /** One dot thin line compatible with Swing default caret */ - public static final String THIN_LINE_CARET = "thin-line-caret"; // NOI18N + public static final String THIN_LINE_CARET = EditorPreferencesDefaults.THIN_LINE_CARET; // NOI18N /** @since 1.23 */ - public static final String THICK_LINE_CARET = "thick-line-caret"; // NOI18N + public static final String THICK_LINE_CARET = EditorPreferencesDefaults.THICK_LINE_CARET; // NOI18N + + /** Default caret type */ + public static final String LINE_CARET = THICK_LINE_CARET; // NOI18N /** Boolean property defining whether selection is being rectangular in a particular text component. */ private static final String RECTANGULAR_SELECTION_PROPERTY = "rectangular-selection"; // NOI18N diff --git a/editor.lib/src/org/netbeans/editor/BaseKit.java b/editor.lib/src/org/netbeans/editor/BaseKit.java --- a/editor.lib/src/org/netbeans/editor/BaseKit.java +++ b/editor.lib/src/org/netbeans/editor/BaseKit.java @@ -59,6 +59,7 @@ import java.util.Iterator; import java.util.List; import java.util.ArrayList; +import java.util.LinkedList; import java.util.prefs.PreferenceChangeEvent; import javax.swing.Action; import javax.swing.InputMap; @@ -88,11 +89,22 @@ import javax.swing.event.ChangeListener; import javax.swing.plaf.TextUI; import javax.swing.text.AbstractDocument; +import static javax.swing.text.DefaultEditorKit.selectionBackwardAction; +import static javax.swing.text.DefaultEditorKit.selectionBeginLineAction; +import static javax.swing.text.DefaultEditorKit.selectionDownAction; +import static javax.swing.text.DefaultEditorKit.selectionEndLineAction; +import static javax.swing.text.DefaultEditorKit.selectionForwardAction; +import static javax.swing.text.DefaultEditorKit.selectionUpAction; import javax.swing.text.EditorKit; import javax.swing.text.Position; import javax.swing.text.View; +import javax.swing.undo.AbstractUndoableEdit; +import javax.swing.undo.CannotRedoException; +import javax.swing.undo.CannotUndoException; +import org.netbeans.api.editor.caret.CaretInfo; import org.netbeans.api.editor.EditorActionRegistration; import org.netbeans.api.editor.EditorActionRegistrations; +import org.netbeans.api.editor.caret.EditorCaret; import org.netbeans.api.editor.mimelookup.MimeLookup; import org.netbeans.api.editor.mimelookup.MimePath; import org.netbeans.api.editor.settings.KeyBindingSettings; @@ -108,8 +120,11 @@ import org.netbeans.modules.editor.lib2.EditorPreferencesKeys; import org.netbeans.modules.editor.lib.KitsTracker; import org.netbeans.api.editor.NavigationHistory; +import org.netbeans.api.editor.caret.CaretMoveContext; +import org.netbeans.spi.editor.caret.CaretMoveHandler; import org.netbeans.lib.editor.util.swing.PositionRegion; import org.netbeans.modules.editor.lib.SettingsConversions; +import org.netbeans.modules.editor.lib2.RectangularSelectionCaretAccessor; import org.netbeans.modules.editor.lib2.RectangularSelectionUtils; import org.netbeans.modules.editor.lib2.actions.KeyBindingsUpdater; import org.netbeans.modules.editor.lib2.typinghooks.DeletedTextInterceptorsManager; @@ -122,6 +137,7 @@ import org.openide.util.LookupEvent; import org.openide.util.LookupListener; import org.openide.util.NbBundle; +import org.openide.util.Pair; import org.openide.util.WeakListeners; import org.openide.util.WeakSet; @@ -545,7 +561,7 @@ /** Create caret to navigate through document */ public @Override Caret createCaret() { - return new BaseCaret(); + return new EditorCaret(); } /** Create empty document */ @@ -1111,8 +1127,8 @@ } }); Caret caret = target.getCaret(); - if (caret instanceof BaseCaret) { - ((BaseCaret)caret).setRectangularSelectionToDotAndMark(); + if (caret instanceof EditorCaret) { + RectangularSelectionCaretAccessor.get().setRectangularSelectionToDotAndMark((EditorCaret)caret); } if (changed[0]) { return; @@ -1120,66 +1136,170 @@ } try { - final Position insertionOffset = doc.createPosition(computeInsertionOffset(target.getCaret()), Position.Bias.Backward); - String replacedText = ""; - if (Utilities.isSelectionShowing(target.getCaret())) { - int p0 = Math.min(target.getCaret().getDot(), target.getCaret().getMark()); - int p1 = Math.max(target.getCaret().getDot(), target.getCaret().getMark()); - replacedText = doc.getText(p0, p1 - p0); - } - final TypedTextInterceptorsManager.Transaction transaction = TypedTextInterceptorsManager.getInstance().openTransaction( - target, insertionOffset, cmd, replacedText); - - try { - if (!transaction.beforeInsertion()) { - final Object [] result = new Object [] { Boolean.FALSE, "" }; //NOI18N - doc.runAtomicAsUser (new Runnable () { - public void run () { - boolean alreadyBeeped = false; - if (Utilities.isSelectionShowing(target.getCaret())) { // valid selection - EditorUI editorUI = Utilities.getEditorUI(target); - Boolean overwriteMode = (Boolean) editorUI.getProperty(EditorUI.OVERWRITE_MODE_PROPERTY); - boolean ovr = (overwriteMode != null && overwriteMode.booleanValue()); +// for (final CaretInfo caret : ((EditorCaret)target.getCaret()).getCarets()) { +// final Position insertionOffset = doc.createPosition(computeInsertionOffset(caret), Position.Bias.Backward); +// String replacedText = ""; +// if (target.getCaret().isSelectionVisible() && caret.getDotPosition() != caret.getMarkPosition()) { +// int p0 = Math.min(caret.getDot(), caret.getMark()); +// int p1 = Math.max(caret.getDot(), caret.getMark()); +// replacedText = doc.getText(p0, p1 - p0); +// } +// final TypedTextInterceptorsManager.Transaction transaction = TypedTextInterceptorsManager.getInstance().openTransaction( +// target, insertionOffset, cmd, replacedText); +// +// try { +// if (!transaction.beforeInsertion()) { +// final Object [] result = new Object [] { Boolean.FALSE, "" }; //NOI18N +// doc.runAtomicAsUser (new Runnable () { +// public void run () { +// boolean alreadyBeeped = false; +// if (target.getCaret().isSelectionVisible() && caret.getDot() != caret.getMark()) { // valid selection +// EditorUI editorUI = Utilities.getEditorUI(target); +// Boolean overwriteMode = (Boolean) editorUI.getProperty(EditorUI.OVERWRITE_MODE_PROPERTY); +// boolean ovr = (overwriteMode != null && overwriteMode.booleanValue()); + final Caret[] caret = {target.getCaret()}; + + if(caret[0] instanceof EditorCaret) { + EditorCaret editorCaret = (EditorCaret) caret[0]; + final List carets = editorCaret.getCarets(); + if(carets.size() > 1) { + doc.runAtomicAsUser(new Runnable() { + public void run() { + boolean alreadyBeeped = false; + DocumentUtilities.setTypingModification(doc, true); try { - doc.putProperty(DOC_REPLACE_SELECTION_PROPERTY, true); - replaceSelection(target, insertionOffset.getOffset(), target.getCaret(), "", ovr); + List dotAndMarkPosPairs = new ArrayList<>(carets.size() << 1); + for (CaretInfo c : carets) { + dotAndMarkPosPairs.add(c.getDotPosition()); + dotAndMarkPosPairs.add(c.getMarkPosition()); + if (c.isSelection()) { // valid selection + int p0 = Math.min(c.getDot(), c.getMark()); + int p1 = Math.max(c.getDot(), c.getMark()); + String replacedText = null; + try { + replacedText = doc.getText(p0, p1 - p0); + } catch (BadLocationException ble) { + LOG.log(Level.FINE, null, ble); + if (!alreadyBeeped) { + target.getToolkit().beep(); + } + alreadyBeeped = true; + } + EditorUI editorUI = Utilities.getEditorUI(target); + Boolean overwriteMode = (Boolean) editorUI.getProperty(EditorUI.OVERWRITE_MODE_PROPERTY); + boolean ovr = (overwriteMode != null && overwriteMode.booleanValue()); + try { + doc.putProperty(DOC_REPLACE_SELECTION_PROPERTY, true); + doc.remove(p0, p1 - p0); + } catch (BadLocationException ble) { + LOG.log(Level.FINE, null, ble); + if (!alreadyBeeped) { + target.getToolkit().beep(); + } + alreadyBeeped = true; + } finally { + doc.putProperty(DOC_REPLACE_SELECTION_PROPERTY, null); + } + } + try { +// TBD: Mark for the last caret? +// try { +// NavigationHistory.getEdits().markWaypoint(target, insertionOffset, false, true); +// } catch (BadLocationException e) { +// LOG.log(Level.WARNING, "Can't add position to the history of edits.", e); //NOI18N +// } + + final BaseDocument doc = (BaseDocument) target.getDocument(); + EditorUI editorUI = Utilities.getEditorUI(target); + editorUI.getWordMatch().clear(); // reset word matching + + int insertionOffset = c.getDot(); + Boolean overwriteMode = (Boolean) editorUI.getProperty(EditorUI.OVERWRITE_MODE_PROPERTY); + boolean ovr = (overwriteMode != null && overwriteMode.booleanValue()); + if (ovr && insertionOffset < doc.getLength() && doc.getChars(insertionOffset, 1)[0] != '\n') { //NOI18N + // overwrite current char + doc.remove(insertionOffset, 1); + doc.insertString(insertionOffset, cmd, null); + } else { // insert mode + doc.insertString(insertionOffset, cmd, null); + } + } catch (BadLocationException ble) { + LOG.log(Level.FINE, null, ble); + if (!alreadyBeeped) { + target.getToolkit().beep(); + } + } + } + doc.addUndoableEdit(new CaretEdit(dotAndMarkPosPairs, target)); + } finally { + DocumentUtilities.setTypingModification(doc, false); + } + } + }); + return; + } + } + + final Position insertionOffset = doc.createPosition(computeInsertionOffset(caret[0]), Position.Bias.Backward); + String replacedText = ""; + if (target.getCaret().isSelectionVisible() && caret[0].getDot() != caret[0].getMark()) { + int p0 = Math.min(caret[0].getDot(), caret[0].getMark()); + int p1 = Math.max(caret[0].getDot(), caret[0].getMark()); + replacedText = doc.getText(p0, p1 - p0); + } + final TypedTextInterceptorsManager.Transaction transaction = TypedTextInterceptorsManager.getInstance().openTransaction( + target, insertionOffset, cmd, replacedText); + + try { + if (!transaction.beforeInsertion()) { + final Object[] result = new Object[]{Boolean.FALSE, ""}; //NOI18N + doc.runAtomicAsUser(new Runnable() { + public void run() { + boolean alreadyBeeped = false; + if (target.getCaret().isSelectionVisible() && caret[0].getDot() != caret[0].getMark()) { // valid selection + EditorUI editorUI = Utilities.getEditorUI(target); + Boolean overwriteMode = (Boolean) editorUI.getProperty(EditorUI.OVERWRITE_MODE_PROPERTY); + boolean ovr = (overwriteMode != null && overwriteMode.booleanValue()); + try { + doc.putProperty(DOC_REPLACE_SELECTION_PROPERTY, true); + replaceSelection(target, insertionOffset.getOffset(), target.getCaret(), "", ovr); + } catch (BadLocationException ble) { + LOG.log(Level.FINE, null, ble); + target.getToolkit().beep(); + alreadyBeeped = true; + } finally { + doc.putProperty(DOC_REPLACE_SELECTION_PROPERTY, null); + } + } + Object[] r = transaction.textTyped(); + String insertionText = r == null ? cmd : (String) r[0]; + int caretPosition = r == null ? -1 : (Integer) r[1]; + + try { + performTextInsertion(target, insertionOffset.getOffset(), insertionText, caretPosition); + result[0] = Boolean.TRUE; + result[1] = insertionText; } catch (BadLocationException ble) { LOG.log(Level.FINE, null, ble); - target.getToolkit().beep(); - alreadyBeeped = true; - } - finally { - doc.putProperty(DOC_REPLACE_SELECTION_PROPERTY, null); + if (!alreadyBeeped) { + target.getToolkit().beep(); + } } } - Object [] r = transaction.textTyped(); - String insertionText = r == null ? cmd : (String) r[0]; - int caretPosition = r == null ? -1 : (Integer) r[1]; - - try { - performTextInsertion(target, insertionOffset.getOffset(), insertionText, caretPosition); - result[0] = Boolean.TRUE; - result[1] = insertionText; - } catch (BadLocationException ble) { - LOG.log(Level.FINE, null, ble); - if (!alreadyBeeped) - target.getToolkit().beep(); - } - } - }); - - if (((Boolean)result[0]).booleanValue()) { - transaction.afterInsertion(); - - // XXX: this is potentially wrong and we may need to call this with - // the original cmd; or maybe only if insertionText == cmd; but maybe - // it does not matter, because nobody seems to be overwriting this method anyway - checkIndent(target, (String)result[1]); - } // else text insertion failed + }); + + if (((Boolean) result[0]).booleanValue()) { + transaction.afterInsertion(); + + // XXX: this is potentially wrong and we may need to call this with + // the original cmd; or maybe only if insertionText == cmd; but maybe + // it does not matter, because nobody seems to be overwriting this method anyway + checkIndent(target, (String) result[1]); + } // else text insertion failed + } + } finally { + transaction.close(); } - } finally { - transaction.close(); - } } catch (BadLocationException ble) { LOG.log(Level.FINE, null, ble); target.getToolkit().beep(); @@ -1548,6 +1668,78 @@ public void run () { DocumentUtilities.setTypingModification(doc, true); try { + if(caret instanceof EditorCaret) { + EditorCaret editorCaret = (EditorCaret) caret; + editorCaret.moveCarets(new CaretMoveHandler() { + @Override + public void moveCarets(CaretMoveContext context) { + for (CaretInfo caretInfo : context.getOriginalSortedCarets()) { + if (caretInfo.isSelection()) { // block selected + try { + int start = Math.min(caretInfo.getDot(), caretInfo.getMark()); + int end = Math.max(caretInfo.getDot(), caretInfo.getMark()); + String replacedText = doc.getText(start, end - start); + if (replacedText.trim().isEmpty()) { + doc.remove(start, end - start); + insertTabString(doc, start); + } else { + boolean selectionAtLineStart = Utilities.getRowStart(doc, start) == start; + changeBlockIndent(doc, start, end, +1); + if (selectionAtLineStart) { + int newSelectionStartOffset = start; + int lineStartOffset = Utilities.getRowStart(doc, start); + if (lineStartOffset != newSelectionStartOffset) { + target.select(lineStartOffset, end); + } + } + } + } catch (GuardedException ge) { + LOG.log(Level.FINE, null, ge); + target.getToolkit().beep(); + } catch (BadLocationException e) { + LOG.log(Level.WARNING, null, e); + } + } else { // no selected text + int dotOffset = caretInfo.getDot(); + try { + // is there any char on this line before cursor? + int indent = Utilities.getRowIndent(doc, dotOffset); + // test whether we should indent + if (indent == -1) { + // find caret column + int caretCol = Utilities.getVisualColumn(doc, dotOffset); + // find next tab column + int nextTabCol = Utilities.getNextTabColumn(doc, dotOffset); + + indenter.reindent(dotOffset); + + dotOffset = caretInfo.getDot(); + int newCaretCol = Utilities.getVisualColumn(doc, dotOffset); + if (newCaretCol <= caretCol) { + // find indent of the first previous non-white row + int upperCol = Utilities.getRowIndent(doc, dotOffset, false); + changeRowIndent(doc, dotOffset, upperCol > nextTabCol ? upperCol : nextTabCol); + // Fix of #32240 + dotOffset = caretInfo.getDot(); + Position newDotPos = doc.createPosition(Utilities.getRowEnd(doc, dotOffset)); + context.setDot(caretInfo, newDotPos); + } + } else { // already chars on the line + insertTabString(doc, dotOffset); + } + } catch (GuardedException ge) { + LOG.log(Level.FINE, null, ge); + target.getToolkit().beep(); + } catch (BadLocationException e) { + // use the same pos + target.getToolkit().beep(); + LOG.log(Level.FINE, null, e); + } + } + } + } + }); + } else { if (Utilities.isSelectionShowing(caret)) { // block selected try { if (target.getSelectedText().trim().isEmpty()) { @@ -1605,6 +1797,7 @@ LOG.log(Level.FINE, null, e); } } + } } finally { DocumentUtilities.setTypingModification(doc, false); } @@ -1802,6 +1995,85 @@ final BaseDocument doc = (BaseDocument)target.getDocument(); final Caret caret = target.getCaret(); + if (caret instanceof EditorCaret) { + final EditorCaret editorCaret = (EditorCaret) caret; + final List carets = editorCaret.getSortedCarets(); + if (carets.size() > 1) { + doc.runAtomicAsUser(new Runnable() { + public void run() { + boolean alreadyBeeped = false; + DocumentUtilities.setTypingModification(doc, true); + try { + List caretsDotAndMarkPositions = new ArrayList<>(carets.size() << 1); + for (CaretInfo c : carets) { + caretsDotAndMarkPositions.add(c.getDotPosition()); + caretsDotAndMarkPositions.add(c.getMarkPosition()); + if (c.isSelection()) { + // remove selection + final int dot = c.getDot(); + final int mark = c.getMark(); + try { + if (RectangularSelectionUtils.isRectangularSelection(target)) { + if (!RectangularSelectionUtils.removeSelection(target)) { + RectangularSelectionUtils.removeChar(target, nextChar); + } + RectangularSelectionCaretAccessor.get().setRectangularSelectionToDotAndMark((EditorCaret)caret); + } else { + doc.remove(Math.min(dot, mark), Math.abs(dot - mark)); + } + } catch (BadLocationException ble) { + LOG.log(Level.FINE, null, ble); + if (!alreadyBeeped) { + target.getToolkit().beep(); + } + alreadyBeeped = true; + } + } else { + final int dot = c.getDot(); + char[] removedChar = null; + + try { + removedChar = nextChar + ? dot < doc.getLength() ? doc.getChars(dot, 1) : null + : dot > 0 ? doc.getChars(dot - 1, 1) : null; + } catch (BadLocationException ble) { + LOG.log(Level.FINE, null, ble); + if (!alreadyBeeped) { + target.getToolkit().beep(); + } + alreadyBeeped = true; + } + + if (removedChar != null) { + final String removedText = String.valueOf(removedChar); + try { + if (nextChar) { // remove next char + doc.remove(dot, 1); + } else { // remove previous char + doc.remove(dot - 1, 1); + } + } catch (BadLocationException ble) { + LOG.log(Level.FINE, null, ble); + if (!alreadyBeeped) { + target.getToolkit().beep(); + } + alreadyBeeped = true; + } + } + } + } + doc.addUndoableEdit(new CaretEdit(caretsDotAndMarkPositions, target)); + } finally { + DocumentUtilities.setTypingModification(doc, false); + } + } + } + ); + return; + } + } + + // Non-multicaret case final int dot = caret.getDot(); final int mark = caret.getMark(); @@ -1811,16 +2083,20 @@ public void run () { DocumentUtilities.setTypingModification(doc, true); try { + List dotAndMarkPosPairs = new ArrayList<>(2); + dotAndMarkPosPairs.add(doc.createPosition(caret.getDot())); + dotAndMarkPosPairs.add(doc.createPosition(caret.getMark())); if (RectangularSelectionUtils.isRectangularSelection(target)) { if (!RectangularSelectionUtils.removeSelection(target)) { RectangularSelectionUtils.removeChar(target, nextChar); } - if (caret instanceof BaseCaret) { - ((BaseCaret)caret).setRectangularSelectionToDotAndMark(); + if (caret instanceof EditorCaret) { + RectangularSelectionCaretAccessor.get().setRectangularSelectionToDotAndMark((EditorCaret)caret); } } else { doc.remove(Math.min(dot, mark), Math.abs(dot - mark)); } + doc.addUndoableEdit(new CaretEdit(dotAndMarkPosPairs, target)); } catch (BadLocationException e) { target.getToolkit().beep(); } finally { @@ -1972,23 +2248,29 @@ // If on last line (without newline) then insert newline at very end temporarily // then cut and remove previous newline. int removeNewlineOffset = -1; - if (!Utilities.isSelectionShowing(target)) { - Element elem = ((AbstractDocument) target.getDocument()).getParagraphElement( - target.getCaretPosition()); - int lineStartOffset = elem.getStartOffset(); - int lineEndOffset = elem.getEndOffset(); - if (lineEndOffset == doc.getLength() + 1) { // Very end - // Temporarily insert extra newline - try { - doc.insertString(lineEndOffset - 1, "\n", null); - if (lineStartOffset > 0) { // Only when not on first line - removeNewlineOffset = lineStartOffset - 1; + Caret caret = target.getCaret(); + boolean disableNoSelectionCopy = + Boolean.getBoolean("org.netbeans.editor.disable.no.selection.copy"); + if(!disableNoSelectionCopy && + (!(caret instanceof EditorCaret)) || !(((EditorCaret)caret).getCarets().size() > 1)) { + if (!Utilities.isSelectionShowing(target)) { + Element elem = ((AbstractDocument) target.getDocument()).getParagraphElement( + target.getCaretPosition()); + int lineStartOffset = elem.getStartOffset(); + int lineEndOffset = elem.getEndOffset(); + if (lineEndOffset == doc.getLength() + 1) { // Very end + // Temporarily insert extra newline + try { + doc.insertString(lineEndOffset - 1, "\n", null); + if (lineStartOffset > 0) { // Only when not on first line + removeNewlineOffset = lineStartOffset - 1; + } + } catch (BadLocationException e) { + // could not insert extra newline } - } catch (BadLocationException e) { - // could not insert extra newline } + target.select(lineStartOffset, lineEndOffset); } - target.select(lineStartOffset, lineEndOffset); } target.cut(); @@ -2023,16 +2305,21 @@ public void actionPerformed(ActionEvent evt, JTextComponent target) { if (target != null) { try { - int caretPosition = target.getCaretPosition(); - boolean emptySelection = !Utilities.isSelectionShowing(target); + Caret caret = target.getCaret(); + boolean emptySelection = false; boolean disableNoSelectionCopy = Boolean.getBoolean("org.netbeans.editor.disable.no.selection.copy"); - // If there is no selection then pre-select a current line including newline - if (emptySelection && !disableNoSelectionCopy) { - Element elem = ((AbstractDocument) target.getDocument()).getParagraphElement( - caretPosition); - if (!Utilities.isRowWhite((BaseDocument) target.getDocument(), elem.getStartOffset())) { - target.select(elem.getStartOffset(), elem.getEndOffset()); + int caretPosition = caret.getDot(); + if(!disableNoSelectionCopy && + (!(caret instanceof EditorCaret)) || !(((EditorCaret)caret).getCarets().size() > 1)) { + emptySelection = !Utilities.isSelectionShowing(target); + // If there is no selection then pre-select a current line including newline + if (emptySelection && !disableNoSelectionCopy) { + Element elem = ((AbstractDocument) target.getDocument()).getParagraphElement( + caretPosition); + if (!Utilities.isRowWhite((BaseDocument) target.getDocument(), elem.getStartOffset())) { + target.select(elem.getStartOffset(), elem.getEndOffset()); + } } } target.copy(); @@ -2214,10 +2501,51 @@ super(ABBREV_RESET | UNDO_MERGE_RESET | WORD_MATCH_RESET | CLEAR_STATUS_TEXT); } - public void actionPerformed(ActionEvent evt, JTextComponent target) { + public void actionPerformed(ActionEvent evt, final JTextComponent target) { if (target != null) { + Caret caret = target.getCaret(); + final Document doc = target.getDocument(); + if(doc != null && caret instanceof EditorCaret) { + final EditorCaret editorCaret = (EditorCaret) caret; + editorCaret.moveCarets(new CaretMoveHandler() { + @Override + public void moveCarets(CaretMoveContext context) { + for (CaretInfo caretInfo : context.getOriginalSortedCarets()) { + try { + int dot = caretInfo.getDot(); + Point p = caretInfo.getMagicCaretPosition(); + if (p == null) { + Rectangle r = target.modelToView(dot); + if (r != null) { + p = new Point(r.x, r.y); + context.setMagicCaretPosition(caretInfo, p); + } else { + return; // model to view failed + } + } + try { + dot = Utilities.getPositionAbove(target, dot, p.x); + Position dotPos = doc.createPosition(dot); + boolean select = selectionUpAction.equals(getValue(Action.NAME)); + if (select) { + context.moveDot(caretInfo, dotPos); + if (RectangularSelectionUtils.isRectangularSelection(target)) { + RectangularSelectionCaretAccessor.get().updateRectangularUpDownSelection(editorCaret); + } + } else { + context.setDot(caretInfo, dotPos); + } + } catch (BadLocationException e) { + // the position stays the same + } + } catch (BadLocationException ex) { + target.getToolkit().beep(); + } + } + } + }); + } else { try { - Caret caret = target.getCaret(); int dot = caret.getDot(); Point p = caret.getMagicCaretPosition(); if (p == null) { @@ -2235,8 +2563,8 @@ if (select) { caret.moveDot(dot); if (RectangularSelectionUtils.isRectangularSelection(target)) { - if (caret instanceof BaseCaret) { - ((BaseCaret) caret).updateRectangularUpDownSelection(); + if (caret instanceof EditorCaret) { + RectangularSelectionCaretAccessor.get().updateRectangularUpDownSelection((EditorCaret)caret); } } } else { @@ -2249,6 +2577,7 @@ target.getToolkit().beep(); } } + } } } @@ -2264,10 +2593,51 @@ super(ABBREV_RESET | UNDO_MERGE_RESET | WORD_MATCH_RESET | CLEAR_STATUS_TEXT); } - public void actionPerformed(ActionEvent evt, JTextComponent target) { + public void actionPerformed(ActionEvent evt, final JTextComponent target) { if (target != null) { + Caret caret = target.getCaret(); + final Document doc = target.getDocument(); + if (doc != null && caret instanceof EditorCaret) { + final EditorCaret editorCaret = (EditorCaret) caret; + editorCaret.moveCarets(new CaretMoveHandler() { + @Override + public void moveCarets(CaretMoveContext context) { + for (CaretInfo caretInfo : context.getOriginalSortedCarets()) { + try { + int dot = caretInfo.getDot(); + Point p = caretInfo.getMagicCaretPosition(); + if (p == null) { + Rectangle r = target.modelToView(dot); + if (r != null) { + p = new Point(r.x, r.y); + context.setMagicCaretPosition(caretInfo, p); + } else { + return; // model to view failed + } + } + try { + dot = Utilities.getPositionBelow(target, dot, p.x); + Position dotPos = doc.createPosition(dot); + boolean select = selectionDownAction.equals(getValue(Action.NAME)); + if (select) { + context.moveDot(caretInfo, dotPos); + if (RectangularSelectionUtils.isRectangularSelection(target)) { + RectangularSelectionCaretAccessor.get().updateRectangularUpDownSelection(editorCaret); + } + } else { + context.setDot(caretInfo, dotPos); + } + } catch (BadLocationException e) { + // position stays the same + } + } catch (BadLocationException ex) { + target.getToolkit().beep(); + } + } + } + }); + } else { try { - Caret caret = target.getCaret(); int dot = caret.getDot(); Point p = caret.getMagicCaretPosition(); if (p == null) { @@ -2285,8 +2655,8 @@ if (select) { caret.moveDot(dot); if (RectangularSelectionUtils.isRectangularSelection(target)) { - if (caret instanceof BaseCaret) { - ((BaseCaret)caret).updateRectangularUpDownSelection(); + if (caret instanceof EditorCaret) { + RectangularSelectionCaretAccessor.get().updateRectangularUpDownSelection((EditorCaret)caret); } } } else { @@ -2298,6 +2668,7 @@ } catch (BadLocationException ex) { target.getToolkit().beep(); } + } } } } @@ -2315,11 +2686,106 @@ super(ABBREV_RESET | UNDO_MERGE_RESET | WORD_MATCH_RESET | CLEAR_STATUS_TEXT); } - public void actionPerformed(ActionEvent evt, JTextComponent target) { + public void actionPerformed(ActionEvent evt, final JTextComponent target) { if (target != null) { try { Caret caret = target.getCaret(); - BaseDocument doc = (BaseDocument)target.getDocument(); + final Document doc = target.getDocument(); + if(doc != null && caret instanceof EditorCaret) { + final EditorCaret editorCaret = (EditorCaret) caret; + editorCaret.moveCarets(new CaretMoveHandler() { + @Override + public void moveCarets(CaretMoveContext context) { + try { + for (CaretInfo caretInfo : context.getOriginalSortedCarets()) { + int caretOffset = caretInfo.getDot(); + Rectangle caretBounds = ((BaseTextUI) target.getUI()).modelToView(target, caretOffset); + if (caretBounds == null) { + return; // Cannot continue reasonably + } + + // Retrieve caret magic position and attempt to retain + // the x-coordinate information and use it + // for setting of the new caret position + Point magicCaretPosition = caretInfo.getMagicCaretPosition(); + if (magicCaretPosition == null) { + magicCaretPosition = new Point(caretBounds.x, caretBounds.y); + } + + Rectangle visibleBounds = target.getVisibleRect(); + int newCaretOffset; + Rectangle newCaretBounds; + + // Check whether caret was contained in the original visible window + if (visibleBounds.contains(caretBounds)) { + // Clone present view bounds + Rectangle newVisibleBounds = new Rectangle(visibleBounds); + // Do viewToModel() and modelToView() with the left top corner + // of the currently visible view. If that line is not fully visible + // then it should be the bottom line of the previous page + // (if it's fully visible then the line above it). + int topLeftOffset = target.viewToModel(new Point( + visibleBounds.x, visibleBounds.y)); + Rectangle topLeftLineBounds = target.modelToView(topLeftOffset); + + // newVisibleBounds.y will hold bottom of new view + if (topLeftLineBounds.y != visibleBounds.y) { + newVisibleBounds.y = topLeftLineBounds.y + topLeftLineBounds.height; + } // Component view starts right at the line boundary + // Go back by the view height + newVisibleBounds.y -= visibleBounds.height; + + // Find the new caret bounds by using relative y position + // on the original caret bounds. If the caret's new relative bounds + // would be visually above the old bounds + // the view should be shifted so that the relative bounds + // are the same (user's eyes do not need to move). + int caretRelY = caretBounds.y - visibleBounds.y; + int caretNewY = newVisibleBounds.y + caretRelY; + newCaretOffset = target.viewToModel(new Point(magicCaretPosition.x, caretNewY)); + newCaretBounds = target.modelToView(newCaretOffset); + if (newCaretBounds.y < caretNewY) { + // Need to go one line down to retain the top line + // of the present newVisibleBounds to be fully visible. + // Attempt to go forward by height of caret + newCaretOffset = target.viewToModel(new Point(magicCaretPosition.x, + newCaretBounds.y + newCaretBounds.height)); + newCaretBounds = target.modelToView(newCaretOffset); + } + + // Shift the new visible bounds so that the caret + // does not visually move + newVisibleBounds.y = newCaretBounds.y - caretRelY; + + // Scroll the window to the requested rectangle + target.scrollRectToVisible(newVisibleBounds); + + } else { // Caret outside of originally visible window + // Shift the dot by the visible bounds height + Point newCaretPoint = new Point(magicCaretPosition.x, + caretBounds.y - visibleBounds.height); + newCaretOffset = target.viewToModel(newCaretPoint); + newCaretBounds = target.modelToView(newCaretOffset); + } + + boolean select = selectionPageUpAction.equals(getValue(Action.NAME)); + Position newCaretPos = doc.createPosition(newCaretOffset); + if (select) { + context.moveDot(caretInfo, newCaretPos); + } else { + context.setDot(caretInfo, newCaretPos); + } + + // Update magic caret position + magicCaretPosition.y = newCaretBounds.y; + context.setMagicCaretPosition(caretInfo, magicCaretPosition); + } + } catch (BadLocationException ex) { + target.getToolkit().beep(); + } + } + }); + } else { int caretOffset = caret.getDot(); Rectangle caretBounds = ((BaseTextUI)target.getUI()).modelToView(target, caretOffset); if (caretBounds == null) { @@ -2400,7 +2866,7 @@ // Update magic caret position magicCaretPosition.y = newCaretBounds.y; caret.setMagicCaretPosition(magicCaretPosition); - + } } catch (BadLocationException ex) { target.getToolkit().beep(); } @@ -2421,9 +2887,51 @@ | WORD_MATCH_RESET | CLEAR_STATUS_TEXT); } - public void actionPerformed(ActionEvent evt, JTextComponent target) { + public void actionPerformed(ActionEvent evt, final JTextComponent target) { if (target != null) { Caret caret = target.getCaret(); + final Document doc = target.getDocument(); + if (doc != null && caret instanceof EditorCaret) { + final EditorCaret editorCaret = (EditorCaret) caret; + editorCaret.moveCarets(new CaretMoveHandler() { + @Override + public void moveCarets(CaretMoveContext context) { + for (CaretInfo caretInfo : context.getOriginalSortedCarets()) { + try { + int offset; + boolean select = selectionForwardAction.equals(getValue(Action.NAME)); + if (!select && Utilities.isSelectionShowing(editorCaret)) { + offset = target.getSelectionEnd(); + if (offset != caretInfo.getDot()) { + offset--; + } else { + // clear the selection, but do not move the cursor + Position pos = doc.createPosition(offset); + context.setDot(caretInfo, pos); + return; + } + } else { + offset = caretInfo.getDot(); + } + int dot = target.getUI().getNextVisualPositionFrom(target, + offset, Position.Bias.Forward, SwingConstants.EAST, null); + Position dotPos = doc.createPosition(dot); + if (select) { + if (RectangularSelectionUtils.isRectangularSelection(target)) { + RectangularSelectionCaretAccessor.get().extendRectangularSelection(editorCaret, true, false); + } else { + context.moveDot(caretInfo, dotPos); + } + } else { + context.setDot(caretInfo, dotPos); + } + } catch (BadLocationException ex) { + target.getToolkit().beep(); + } + } + } + }); + } else { try { int pos; boolean select = selectionForwardAction.equals(getValue(Action.NAME)); @@ -2443,8 +2951,8 @@ int dot = target.getUI().getNextVisualPositionFrom(target, pos, Position.Bias.Forward, SwingConstants.EAST, null); if (select) { - if (caret instanceof BaseCaret && RectangularSelectionUtils.isRectangularSelection(target)) { - ((BaseCaret)caret).extendRectangularSelection(true, false); + if (caret instanceof EditorCaret && RectangularSelectionUtils.isRectangularSelection(target)) { + RectangularSelectionCaretAccessor.get().extendRectangularSelection((EditorCaret)caret, true, false); } else { caret.moveDot(dot); } @@ -2455,6 +2963,7 @@ target.getToolkit().beep(); } } + } } } @@ -2472,10 +2981,101 @@ | CLEAR_STATUS_TEXT); } - public void actionPerformed(ActionEvent evt, JTextComponent target) { + public void actionPerformed(ActionEvent evt, final JTextComponent target) { if (target != null) { try { Caret caret = target.getCaret(); + final Document doc = target.getDocument(); + if(doc != null && caret instanceof EditorCaret) { + final EditorCaret editorCaret = (EditorCaret) caret; + editorCaret.moveCarets(new CaretMoveHandler() { + @Override + public void moveCarets(CaretMoveContext context) { + try { + for (CaretInfo caretInfo : context.getOriginalSortedCarets()) { + int caretOffset = caretInfo.getDot(); + Rectangle caretBounds = ((BaseTextUI) target.getUI()).modelToView(target, caretOffset); + if (caretBounds == null) { + return; // Cannot continue reasonably + } + + // Retrieve caret magic position and attempt to retain + // the x-coordinate information and use it + // for setting of the new caret position + Point magicCaretPosition = caretInfo.getMagicCaretPosition(); + if (magicCaretPosition == null) { + magicCaretPosition = new Point(caretBounds.x, caretBounds.y); + } + + Rectangle visibleBounds = target.getVisibleRect(); + int newCaretOffset; + Rectangle newCaretBounds; + + // Check whether caret was contained in the original visible window + if (visibleBounds.contains(caretBounds)) { + // Clone present view bounds + Rectangle newVisibleBounds = new Rectangle(visibleBounds); + // Do viewToModel() and modelToView() with the left bottom corner + // of the currently visible view. + // That line should be the top line of the next page. + int bottomLeftOffset = target.viewToModel(new Point( + visibleBounds.x, visibleBounds.y + visibleBounds.height)); + Rectangle bottomLeftLineBounds = target.modelToView(bottomLeftOffset); + + // newVisibleBounds.y will hold bottom of new view + newVisibleBounds.y = bottomLeftLineBounds.y; + + // Find the new caret bounds by using relative y position + // on the original caret bounds. If the caret's new relative bounds + // would be visually below the old bounds + // the view should be shifted so that the relative bounds + // are the same (user's eyes do not need to move). + int caretRelY = caretBounds.y - visibleBounds.y; + int caretNewY = newVisibleBounds.y + caretRelY; + newCaretOffset = target.viewToModel(new Point(magicCaretPosition.x, caretNewY)); + newCaretBounds = target.modelToView(newCaretOffset); + if (newCaretBounds.y > caretNewY) { + // Need to go one line above to retain the top line + // of the present newVisibleBounds to be fully visible. + // Attempt to go up by height of caret. + newCaretOffset = target.viewToModel(new Point(magicCaretPosition.x, + newCaretBounds.y - newCaretBounds.height)); + newCaretBounds = target.modelToView(newCaretOffset); + } + + // Shift the new visible bounds so that the caret + // does not visually move + newVisibleBounds.y = newCaretBounds.y - caretRelY; + + // Scroll the window to the requested rectangle + target.scrollRectToVisible(newVisibleBounds); + + } else { // Caret outside of originally visible window + // Shift the dot by the visible bounds height + Point newCaretPoint = new Point(magicCaretPosition.x, + caretBounds.y + visibleBounds.height); + newCaretOffset = target.viewToModel(newCaretPoint); + newCaretBounds = target.modelToView(newCaretOffset); + } + + boolean select = selectionPageDownAction.equals(getValue(Action.NAME)); + Position newCaretPos = doc.createPosition(newCaretOffset); + if (select) { + context.moveDot(caretInfo, newCaretPos); + } else { + context.setDot(caretInfo, newCaretPos); + } + + // Update magic caret position + magicCaretPosition.y = newCaretBounds.y; + context.setMagicCaretPosition(caretInfo, magicCaretPosition); + } + } catch (BadLocationException ex) { + target.getToolkit().beep(); + } + } + }); + } else { int caretOffset = caret.getDot(); Rectangle caretBounds = ((BaseTextUI)target.getUI()).modelToView(target, caretOffset); if (caretBounds == null) { @@ -2551,56 +3151,6 @@ // Update magic caret position magicCaretPosition.y = newCaretBounds.y; caret.setMagicCaretPosition(magicCaretPosition); - - } catch (BadLocationException ex) { - target.getToolkit().beep(); - } - } - } - } - - @EditorActionRegistrations({ - @EditorActionRegistration(name = backwardAction), - @EditorActionRegistration(name = selectionBackwardAction) - }) - public static class BackwardAction extends LocalBaseAction { - - static final long serialVersionUID =-3048379822817847356L; - - public BackwardAction() { - super(MAGIC_POSITION_RESET | ABBREV_RESET | UNDO_MERGE_RESET - | WORD_MATCH_RESET | CLEAR_STATUS_TEXT); - } - - public void actionPerformed(ActionEvent evt, JTextComponent target) { - if (target != null) { - Caret caret = target.getCaret(); - try { - int pos; - boolean select = selectionBackwardAction.equals(getValue(Action.NAME)); - if (!select && Utilities.isSelectionShowing(caret)) - { - pos = target.getSelectionStart(); - if (pos != caret.getDot()) { - pos++; - } else { - // clear the selection, but do not move the cursor - caret.setDot(pos); - return; - } - } - else - pos = caret.getDot(); - int dot = target.getUI().getNextVisualPositionFrom(target, - pos, Position.Bias.Backward, SwingConstants.WEST, null); - if (select) { - if (caret instanceof BaseCaret && RectangularSelectionUtils.isRectangularSelection(target)) { - ((BaseCaret)caret).extendRectangularSelection(false, false); - } else { - caret.moveDot(dot); - } - } else { - caret.setDot(dot); } } catch (BadLocationException ex) { target.getToolkit().beep(); @@ -2609,6 +3159,93 @@ } } + @EditorActionRegistrations({ + @EditorActionRegistration(name = backwardAction), + @EditorActionRegistration(name = selectionBackwardAction) + }) + public static class BackwardAction extends LocalBaseAction { + + static final long serialVersionUID = -3048379822817847356L; + + public BackwardAction() { + super(MAGIC_POSITION_RESET | ABBREV_RESET | UNDO_MERGE_RESET + | WORD_MATCH_RESET | CLEAR_STATUS_TEXT); + } + + public void actionPerformed(ActionEvent evt, final JTextComponent target) { + if (target != null) { + Caret caret = target.getCaret(); + final Document doc = target.getDocument(); + if (doc != null && caret instanceof EditorCaret) { + EditorCaret editorCaret = (EditorCaret) caret; + editorCaret.moveCarets(new CaretMoveHandler() { + @Override + public void moveCarets(CaretMoveContext context) { + for (CaretInfo caretInfo : context.getOriginalSortedCarets()) { + try { + int offset; + boolean select = selectionBackwardAction.equals(getValue(Action.NAME)); + if (!select && caretInfo.isSelectionShowing()) { + offset = caretInfo.getSelectionStart(); + if (offset != caretInfo.getDot()) { + offset++; + } else { + // clear the selection, but do not move the cursor + context.setDot(caretInfo, doc.createPosition(offset)); + return; + } + } else { + offset = caretInfo.getDot(); + } + int dot = target.getUI().getNextVisualPositionFrom(target, + offset, Position.Bias.Backward, SwingConstants.WEST, null); + Position dotPos = doc.createPosition(dot); + if (select) { + context.moveDot(caretInfo, dotPos); + } else { + context.setDot(caretInfo, dotPos); + } + } catch (BadLocationException ex) { + target.getToolkit().beep(); + } + } + } + }); + } else { + try { + int pos; + boolean select = selectionBackwardAction.equals(getValue(Action.NAME)); + if (!select && Utilities.isSelectionShowing(caret)) { + pos = target.getSelectionStart(); + if (pos != caret.getDot()) { + pos++; + } else { + // clear the selection, but do not move the cursor + caret.setDot(pos); + return; + } + } else { + pos = caret.getDot(); + } + int dot = target.getUI().getNextVisualPositionFrom(target, + pos, Position.Bias.Backward, SwingConstants.WEST, null); + if (select) { + if (caret instanceof EditorCaret && RectangularSelectionUtils.isRectangularSelection(target)) { + RectangularSelectionCaretAccessor.get().extendRectangularSelection((EditorCaret)caret, false, false); + } else { + caret.moveDot(dot); + } + } else { + caret.setDot(dot); + } + } catch (BadLocationException ex) { + target.getToolkit().beep(); + } + } + } + } + } + public static class BeginLineAction extends LocalBaseAction { @EditorActionRegistrations({ @@ -2639,10 +3276,80 @@ homeKeyColumnOne = columnOne; } - public void actionPerformed(ActionEvent evt, JTextComponent target) { + public void actionPerformed(ActionEvent evt, final JTextComponent target) { if (target != null) { Caret caret = target.getCaret(); - BaseDocument doc = (BaseDocument)target.getDocument(); + final Document doc = target.getDocument(); + if (doc != null && caret instanceof EditorCaret) { + EditorCaret editorCaret = (EditorCaret) caret; + editorCaret.moveCarets(new CaretMoveHandler() { + @Override + public void moveCarets(CaretMoveContext context) { + for (CaretInfo caretInfo : context.getOriginalSortedCarets()) { + try { + int dot = caretInfo.getDot(); + // #232675: if bounds are defined, use them rather than line start/end + Object o = target.getClientProperty(PROP_NAVIGATE_BOUNDARIES); + PositionRegion bounds = null; + if (o instanceof PositionRegion) { + bounds = (PositionRegion) o; + int start = bounds.getStartOffset(); + int end = bounds.getEndOffset(); + if (dot > start && dot <= end) { + // move to the region start + dot = start; + } else { + bounds = null; + } + } + + if (bounds == null) { + int lineStartPos = Utilities.getRowStart(target, dot); + if (homeKeyColumnOne) { // to first column + dot = lineStartPos; + } else { // either to line start or text start + BaseDocument doc = (BaseDocument) target.getDocument(); + int textStartPos = Utilities.getRowFirstNonWhite(doc, lineStartPos); + if (textStartPos < 0) { // no text on the line + textStartPos = Utilities.getRowEnd(target, lineStartPos); + } + if (dot == lineStartPos) { // go to the text start pos + dot = textStartPos; + } else if (dot <= textStartPos) { + dot = lineStartPos; + } else { + dot = textStartPos; + } + } + } + // For partial view hierarchy check bounds + dot = Math.max(dot, target.getUI().getRootView(target).getStartOffset()); + String actionName = (String) getValue(Action.NAME); + boolean select = selectionBeginLineAction.equals(actionName) + || selectionLineFirstColumnAction.equals(actionName); + + // If possible scroll the view to its begining horizontally + // to ease user's orientation in the code. + Rectangle r = target.modelToView(dot); + Rectangle visRect = target.getVisibleRect(); + if (r.getMaxX() < visRect.getWidth()) { + r.x = 0; + target.scrollRectToVisible(r); + } + + Position dotPos = doc.createPosition(dot); + if (select) { + context.moveDot(caretInfo, dotPos); + } else { + context.setDot(caretInfo, dotPos); + } + } catch (BadLocationException e) { + target.getToolkit().beep(); + } + } + } + }); + } else { try { int dot = caret.getDot(); // #232675: if bounds are defined, use them rather than line start/end @@ -2665,7 +3372,7 @@ if (homeKeyColumnOne) { // to first column dot = lineStartPos; } else { // either to line start or text start - int textStartPos = Utilities.getRowFirstNonWhite(doc, lineStartPos); + int textStartPos = Utilities.getRowFirstNonWhite(((BaseDocument)doc), lineStartPos); if (textStartPos < 0) { // no text on the line textStartPos = Utilities.getRowEnd(target, lineStartPos); } @@ -2702,6 +3409,7 @@ target.getToolkit().beep(); } } + } } } @@ -2718,9 +3426,58 @@ | WORD_MATCH_RESET | CLEAR_STATUS_TEXT); } - public void actionPerformed(ActionEvent evt, JTextComponent target) { + public void actionPerformed(ActionEvent evt, final JTextComponent target) { if (target != null) { Caret caret = target.getCaret(); + if(caret instanceof EditorCaret) { + ((EditorCaret) caret).moveCarets(new org.netbeans.spi.editor.caret.CaretMoveHandler() { + @Override + public void moveCarets(CaretMoveContext context) { + for (CaretInfo caretInfo : context.getOriginalSortedCarets()) { + try { + // #232675: if bounds are defined, use them rather than line start/end + Object o = target.getClientProperty(PROP_NAVIGATE_BOUNDARIES); + int dot = -1; + + if (o instanceof PositionRegion) { + PositionRegion bounds = (PositionRegion) o; + int start = bounds.getStartOffset(); + int end = bounds.getEndOffset(); + int d = caretInfo.getDot(); + if (d >= start && d < end) { + // move to the region start + dot = end; + } + } + + if (dot == -1) { + dot = Utilities.getRowEnd(target, caretInfo.getDot()); + } + + // For partial view hierarchy check bounds + dot = Math.min(dot, target.getUI().getRootView(target).getEndOffset()); + boolean select = selectionEndLineAction.equals(getValue(Action.NAME)); + Position dotPos = context.getDocument().createPosition(dot); + if (select) { + context.moveDot(caretInfo, dotPos); + } else { + context.setDot(caretInfo, dotPos); + } + // now move the magic caret position far to the right + Rectangle r = target.modelToView(dot); + if (r != null) { + Point p = new Point(MAGIC_POSITION_MAX, r.y); + context.setMagicCaretPosition(caretInfo, p); + } + } catch (BadLocationException e) { + e.printStackTrace(); + target.getToolkit().beep(); + } + } + } + }); + + } else { try { // #232675: if bounds are defined, use them rather than line start/end Object o = target.getClientProperty(PROP_NAVIGATE_BOUNDARIES); @@ -2760,6 +3517,7 @@ target.getToolkit().beep(); } } + } } } @@ -2852,17 +3610,13 @@ int newDotOffset = getNextWordOffset(target); boolean select = selectionNextWordAction.equals(getValue(Action.NAME)); if (select) { - if (caret instanceof BaseCaret && RectangularSelectionUtils.isRectangularSelection(target)) { - ((BaseCaret) caret).extendRectangularSelection(true, true); + if (caret instanceof EditorCaret && RectangularSelectionUtils.isRectangularSelection(target)) { + RectangularSelectionCaretAccessor.get().extendRectangularSelection((EditorCaret)caret, true, true); } else { caret.moveDot(newDotOffset); } } else { - if (caret instanceof BaseCaret) { - ((BaseCaret)caret).setDot(newDotOffset, false); - } else { - caret.setDot(newDotOffset); - } + caret.setDot(newDotOffset); } } catch (BadLocationException ex) { target.getToolkit().beep(); @@ -2898,17 +3652,13 @@ int newDotOffset = getPreviousWordOffset(target); boolean select = selectionPreviousWordAction.equals(getValue(Action.NAME)); if (select) { - if (caret instanceof BaseCaret && RectangularSelectionUtils.isRectangularSelection(target)) { - ((BaseCaret) caret).extendRectangularSelection(false, true); + if (caret instanceof EditorCaret && RectangularSelectionUtils.isRectangularSelection(target)) { + RectangularSelectionCaretAccessor.get().extendRectangularSelection((EditorCaret)caret, false, true); } else { caret.moveDot(newDotOffset); } } else { - if (caret instanceof BaseCaret) { - ((BaseCaret)caret).setDot(newDotOffset, false); - } else { - caret.setDot(newDotOffset); - } + caret.setDot(newDotOffset); } } catch (BadLocationException ex) { target.getToolkit().beep(); @@ -3008,7 +3758,7 @@ if (target != null) { final Caret caret = target.getCaret(); final BaseDocument doc = (BaseDocument)target.getDocument(); - doc.runAtomicAsUser (new Runnable () { + doc.render(new Runnable () { public void run () { DocumentUtilities.setTypingModification(doc, true); try { @@ -3171,6 +3921,29 @@ } } } // End of DefaultSyntaxTokenContext class + + private static class CaretEdit extends AbstractUndoableEdit { + + private final JTextComponent target; + private final List offsets; + + public CaretEdit(List offsets, JTextComponent target) { + this.target = target; + this.offsets = offsets; + } + + @Override + public void undo() throws CannotUndoException { + EditorCaret caret = (EditorCaret) target.getCaret(); + caret.replaceCarets(offsets); + } + + @Override + public void redo() throws CannotRedoException { + EditorCaret caret = (EditorCaret) target.getCaret(); + caret.replaceCarets(offsets); + } + } private class KeybindingsAndPreferencesTracker implements LookupListener, PreferenceChangeListener { diff --git a/editor.lib/src/org/netbeans/editor/EditorUI.java b/editor.lib/src/org/netbeans/editor/EditorUI.java --- a/editor.lib/src/org/netbeans/editor/EditorUI.java +++ b/editor.lib/src/org/netbeans/editor/EditorUI.java @@ -82,6 +82,7 @@ import javax.swing.plaf.TextUI; import javax.swing.text.*; import javax.swing.undo.UndoManager; +import org.netbeans.api.editor.EditorUtilities; import org.netbeans.api.editor.mimelookup.MimeLookup; import org.netbeans.api.editor.mimelookup.MimePath; import org.netbeans.api.editor.settings.EditorStyleConstants; @@ -655,7 +656,10 @@ if (!component.isEnabled()) { component.getCaret().setVisible(false); } - } + } else if (EditorUtilities.CARET_OVERWRITE_MODE_PROPERTY.equals(propName)) { + // Mirror the property into EditorUI + putProperty(OVERWRITE_MODE_PROPERTY, component.getClientProperty(EditorUtilities.CARET_OVERWRITE_MODE_PROPERTY)); + } if (propName == null || ColoringMap.PROP_COLORING_MAP.equals(propName)) { listener.preferenceChange(null); diff --git a/editor.lib/src/org/netbeans/editor/ext/ExtKit.java b/editor.lib/src/org/netbeans/editor/ext/ExtKit.java --- a/editor.lib/src/org/netbeans/editor/ext/ExtKit.java +++ b/editor.lib/src/org/netbeans/editor/ext/ExtKit.java @@ -177,11 +177,6 @@ public ExtKit() { } - /** Create caret to navigate through document */ - public @Override Caret createCaret() { - return new ExtCaret(); - } - public @Override SyntaxSupport createSyntaxSupport(BaseDocument doc) { return new ExtSyntaxSupport(doc); } diff --git a/editor.lib2/apichanges.xml b/editor.lib2/apichanges.xml --- a/editor.lib2/apichanges.xml +++ b/editor.lib2/apichanges.xml @@ -105,8 +105,26 @@ - + + + Caret API introduced + + + + + + +

+ The Caret API was introduced to allow working with multiple + carets within one document. +

+
+ + + + +
Added IDs for goto prev/next occurrence actions @@ -122,6 +140,22 @@ + + Extended HightlightsSequence by ShiftHighlightsSequence + + + + + +

+ Highlights sequence that supports shifts in addition to regular offsets. + This allows to color individual spaces within a tab character + or to color extra virtual characters beyond a newline character. +

+
+ + +
ReleasableHighlightsContainer added diff --git a/editor.lib2/arch.xml b/editor.lib2/arch.xml --- a/editor.lib2/arch.xml +++ b/editor.lib2/arch.xml @@ -87,7 +87,10 @@
  • Typing Hooks SPI -
  • + +
  • + Editor Caret API +
  • @@ -126,8 +129,8 @@ --> - At the moment the Editor Library 2 module contains three distinct SPIs. Each SPI lives - in its own package and the usecases can be found in the packages overview. + At the moment the Editor Library 2 module contains distinct APIs/SPIs. They live + in their own package and the usecases can be found in the packages overview. diff --git a/editor.lib2/nbproject/project.xml b/editor.lib2/nbproject/project.xml --- a/editor.lib2/nbproject/project.xml +++ b/editor.lib2/nbproject/project.xml @@ -90,7 +90,7 @@ 1 - 1.44 + 1.63
    @@ -127,14 +127,6 @@ - org.openide.util.ui - - - - 9.3 - - - org.openide.util @@ -150,6 +142,14 @@ 8.0 + + org.openide.util.ui + + + + 9.3 + + @@ -209,8 +209,9 @@ org.netbeans.api.editor - org.netbeans.api.editor.document + org.netbeans.api.editor.caret org.netbeans.spi.editor + org.netbeans.spi.editor.caret org.netbeans.spi.editor.codegen org.netbeans.spi.editor.document org.netbeans.spi.editor.highlighting diff --git a/editor.lib2/src/org/netbeans/api/editor/EditorActionNames.java b/editor.lib2/src/org/netbeans/api/editor/EditorActionNames.java --- a/editor.lib2/src/org/netbeans/api/editor/EditorActionNames.java +++ b/editor.lib2/src/org/netbeans/api/editor/EditorActionNames.java @@ -81,7 +81,7 @@ /** * Zoom text in by increasing default font size. - *
    + *
    * textComponent.getClientProperty("text-zoom") contains positive (or negative) * integer of how many points the font size should be increased (decreased). * @since 1.45 @@ -136,6 +136,19 @@ * @since 1.64 */ public static final String organizeMembers = "organize-members"; // NOI18N + + + /** + * Toggle caret between regular insert mode and overwrite mode. + * @since 1.31 + */ + public static final String toggleTypingMode = "toggle-typing-mode"; // NOI18N + + /** + * Remove the last added caret. + * @since 2.6 + */ + public static final String removeLastCaret = "remove-last-caret"; // NOI18N /** * Navigates to the previous occurence of the symbol under the caret. The action @@ -150,4 +163,17 @@ * @since 2.3 */ public static final String gotoNextOccurrence = "next-marked-occurrence"; // NOI18N + + /** + * Add a caret to the line above at the same position. + * @since 2.6 + */ + public static final String addSelectionElseCaretUpAction = "add-selection-else-caret-up"; // NOI18N + + /** + * Add a caret to the line below at the same position. + * @since 2.6 + */ + public static final String addSelectionElseCaretDownAction = "add-selection-else-caret-down"; // NOI18N + } diff --git a/editor.lib2/src/org/netbeans/api/editor/EditorActionRegistration.java b/editor.lib2/src/org/netbeans/api/editor/EditorActionRegistration.java --- a/editor.lib2/src/org/netbeans/api/editor/EditorActionRegistration.java +++ b/editor.lib2/src/org/netbeans/api/editor/EditorActionRegistration.java @@ -52,7 +52,7 @@ /** * Registration of an editor action so that it's automatically added into the list * of editor actions even without being explicitly created by BaseKit.createActions(). - *
    + *
    * The corresponding annotation processor will build a xml-layer entry file * in the corresponding /Editors/<mime-type>/Actions folder. * @@ -65,7 +65,7 @@ /** * Name of the action that will appear as Action.NAME attribute's value. - *
    + *
    * The Swing's text package actions use convention of lowercase letters with hyphens * e.g. "caret-end-word" - see String constants in {@link javax.swing.text.DefaultEditorKit}. * @@ -75,7 +75,7 @@ /** * Mime type for which the action will be registered. - *
    + *
    * It implies the target folder of the registration /Editors/<mime-type>/Actions. * * @return mime-type of the action registration (for example "text/x-java" @@ -117,10 +117,10 @@ * Menu text bundle key of the registered action. * If an empty string is used (the default) it will be set to the same value * like action's short description. - *
    + *
    * Value starting with a hash "#key" searches in a bundle in the same package * as the action's class. - *
    + *
    * "bundle#key" allows specification of both bundle and a corresponding key. */ String menuText() default ""; @@ -129,10 +129,10 @@ * Popup menu text bundle key of the registered action. * If an empty string is used (the default) it will be set to the same value * like menu text. - *
    + *
    * Value starting with a hash "#key" searches in a bundle in the same package * as the action's class. - *
    + *
    * "bundle#key" allows specification of both bundle and a corresponding key. */ String popupText() default ""; @@ -144,7 +144,7 @@ /** * Integer position of the main menu item among the other menu items. - *
    + *
    * The default Integer.MAX_VALUE value means no menu representation. */ int menuPosition() default Integer.MAX_VALUE; @@ -157,21 +157,21 @@ /** * Integer position of the popup menu item among the other popup menu (or submenu) items. - *
    + *
    * The default Integer.MAX_VALUE value means no popup menu representation. */ int popupPosition() default Integer.MAX_VALUE; /** * Integer position of this action in editor toolbar. - *
    + *
    * The default Integer.MAX_VALUE value means no toolbar representation. */ int toolBarPosition() default Integer.MAX_VALUE; /** * True if the action should not display its icon in menu. - *
    + *
    * False by default (icon visible in menu). * @since 1.74 */ @@ -179,7 +179,7 @@ /** * True if the action should not be displayed in customizer for key bindings assignment. - *
    + *
    * False by default (key binding can be configured for the action). * @since 1.74 */ @@ -187,7 +187,7 @@ /** * Boolean key in preferences that corresponds to action's selected state. - *
    + *
    * If set to non-empty string the action will be represented by a check-box * in menu and popup menu and the corresponding key will be set in * global mime-lookup MimeLookup.getLookup(MimePath.EMPTY). @@ -196,7 +196,7 @@ /** * Whether or not the action should be in checked state by default. - *
    + *
    * If the preference should default to true or to false. Only valid in conjunction * with {@link #preferencesKey() }. * diff --git a/editor.lib2/src/org/netbeans/api/editor/EditorActionRegistrations.java b/editor.lib2/src/org/netbeans/api/editor/EditorActionRegistrations.java --- a/editor.lib2/src/org/netbeans/api/editor/EditorActionRegistrations.java +++ b/editor.lib2/src/org/netbeans/api/editor/EditorActionRegistrations.java @@ -52,7 +52,7 @@ /** * Annotation allowing to annotate one action class by multiple * {@link EditorActionRegistration} annotations. - *
    + *
    * Example: *
      * @EditorActionRegistrations({ @EditorActionRegistration(name = "name1", ...),
    diff --git a/editor.lib2/src/org/netbeans/api/editor/EditorRegistry.java b/editor.lib2/src/org/netbeans/api/editor/EditorRegistry.java
    --- a/editor.lib2/src/org/netbeans/api/editor/EditorRegistry.java
    +++ b/editor.lib2/src/org/netbeans/api/editor/EditorRegistry.java
    @@ -72,12 +72,12 @@
     
     /**
      * Registry maintaining {@link JTextComponent}s in most-recently-used order.
    - * 
    + *
    * The particular text component needs to register itself first (to avoid dealing * with all the JTextFields etc.). Then the registry will attach * a focus listener to the text component and once the component gains * the focus it will move to the head of the components list. - *
    + *
    * The registry will also fire a change in case a document property * of the focused component changes (by calling component.setDocument()). * @@ -98,9 +98,9 @@ /** * Fired when focus was delivered to a registered text component. - *
    + *
    * The focused component will become the first in the components list. - *
    + *
    * The {@link java.beans.PropertyChangeEvent#getOldValue()} will be a component * losing the focus {@link FocusEvent#getOppositeComponent()}. * The {@link java.beans.PropertyChangeEvent#getNewValue()} will be the text component gaining the focus. @@ -109,9 +109,9 @@ /** * Fired when a registered focused component has lost the focus. - *
    + *
    * The focused component will remain the first in the components list. - *
    + *
    * The {@link java.beans.PropertyChangeEvent#getOldValue()} will be the text component * losing the focus and the {@link java.beans.PropertyChangeEvent#getNewValue()} * will be the component gaining the focus {@link FocusEvent#getOppositeComponent()}. @@ -121,7 +121,7 @@ /** * Fired when document property of the focused component changes * i.e. someone has called {@link JTextComponent#setDocument(Document)}. - *
    + *
    * The {@link java.beans.PropertyChangeEvent#getOldValue()} will be the original document * of the focused text component and the {@link java.beans.PropertyChangeEvent#getNewValue()} * will be the new document set to the focused text component. @@ -132,13 +132,13 @@ * Fired when a component (returned previously from {@link #componentList()}) * is removed from component hierarchy (so it's likely that the component will be released completely * and garbage-collected). - *
    + *
    * Such component will no longer be returned from {@link #componentList()} * or {@link #lastFocusedComponent()}. - *
    + *
    * The {@link java.beans.PropertyChangeEvent#getOldValue()} will be the removed * component. - *
    + *
    * The {@link java.beans.PropertyChangeEvent#getNewValue()} returns null */ public static final String COMPONENT_REMOVED_PROPERTY = "componentRemoved"; //NOI18N @@ -147,14 +147,14 @@ * Fired when the last focused component (returned previously from {@link #lastFocusedComponent()}) * was removed from component hierarchy (so it's likely that the component will be released completely * and garbage-collected). - *
    + *
    * Such component will no longer be returned from {@link #componentList()} * or {@link #lastFocusedComponent()}. - *
    + *
    * The {@link java.beans.PropertyChangeEvent#getOldValue()} will be the removed * last focused component and the {@link java.beans.PropertyChangeEvent#getNewValue()} * will be the component that would currently be returned from {@link #lastFocusedComponent()}. - *
    + *
    * If {@link java.beans.PropertyChangeEvent#getNewValue()} returns null * then there are no longer any registered components * ({@link #componentList()} would return empty list). If the client @@ -180,7 +180,7 @@ /** * Return last focused text component (from the ones included in the registry). - *
    + *
    * It may or may not currently have a focus. * * @return last focused text component or null if no text components @@ -193,7 +193,7 @@ /** * Return the last focused component if it currently has a focus * or return null if none of the registered components currently have the focus. - *
    + *
    * @return focused component or null if none of the registered components * is currently focused. */ @@ -205,7 +205,7 @@ /** * Get list of all components present in the registry starting with the most active * and ending with least active component. - *
    + *
    * The list is a snapshot of the current state and it may be modified * by the caller if desired. * @@ -240,7 +240,7 @@ *
  • {@link #FOCUS_LOST_PROPERTY}
  • *
  • {@link #FOCUSED_DOCUMENT_PROPERTY}
  • * . - *
    + *
    * All the firing should occur in AWT thread only * (assuming the JTextComponent.setDocument() is done properly in AWT). * diff --git a/editor.lib2/src/org/netbeans/api/editor/EditorUtilities.java b/editor.lib2/src/org/netbeans/api/editor/EditorUtilities.java --- a/editor.lib2/src/org/netbeans/api/editor/EditorUtilities.java +++ b/editor.lib2/src/org/netbeans/api/editor/EditorUtilities.java @@ -57,6 +57,15 @@ */ public final class EditorUtilities { + + /** + * Client property of editor component which determines + * whether caret is currently in overwrite mode (Boolean.TRUE) or insert mode (Boolean.FALSE or null). + *
    + * It's modified by appropriate editor actions. + * @since 2.6 + */ + public static final String CARET_OVERWRITE_MODE_PROPERTY = "caret-overwrite-mode"; private EditorUtilities() { // No instances diff --git a/editor.lib2/src/org/netbeans/api/editor/caret/CaretInfo.java b/editor.lib2/src/org/netbeans/api/editor/caret/CaretInfo.java new file mode 100644 --- /dev/null +++ b/editor.lib2/src/org/netbeans/api/editor/caret/CaretInfo.java @@ -0,0 +1,182 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2015 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 2015 Sun Microsystems, Inc. + */ +package org.netbeans.api.editor.caret; + +import java.awt.Point; +import java.util.logging.Logger; +import javax.swing.text.Position; +import org.netbeans.api.annotations.common.CheckForNull; + +/** + * Immutable info about a single caret used in caret API - see {@link EditorCaret}. + *
    + * There is one-to-one reference between immutable caret info and mutable {@link CaretItem}. + * CaretItem is not accessible through the caret API and it's managed privately by EditorCaret. + * Once the caret item gets mutated its corresponding caret info becomes obsolete + * and new caret info instance gets created lazily. + * + * @author Miloslav Metelka + * @author Ralph Ruijs + * @see EditorCaret + * @since 2.6 + */ +public final class CaretInfo { + + // -J-Dorg.netbeans.api.editor.CaretInfo.level=FINEST + private static final Logger LOG = Logger.getLogger(CaretInfo.class.getName()); + + private final CaretItem caretItem; + + private final Position dotPos; + + private final Position markPos; + + private final Point magicCaretPosition; + + CaretInfo(CaretItem caretItem) { + this.caretItem = caretItem; + this.dotPos = caretItem.getDotPosition(); + this.markPos = caretItem.getMarkPosition(); + this.magicCaretPosition = caretItem.getMagicCaretPosition(); + } + + /** + * Get position of the caret itself. + * @return non-null position of the caret placement. The position may be virtual + * so methods in {@link org.netbeans.api.editor.document.ShiftPositions} may be used if necessary. + */ + @CheckForNull + public Position getDotPosition() { + return dotPos; + } + + /** + * Return either the same object like {@link #getDotPosition()} if there's no selection + * or return position denoting the other end of an existing selection (which is either before + * or after the dot position depending of how the selection was created). + * @return non-null position of the caret placement. The position may be virtual + * so methods in {@link org.netbeans.api.editor.document.ShiftPositions} may be used if necessary. + */ + @CheckForNull + public Position getMarkPosition() { + return markPos; + } + + /** + * Fetches the current position of the caret. + * + * @return the position >=0 + */ + public int getDot() { + return (dotPos != null) ? dotPos.getOffset() : 0; + } + + /** + * Fetches the current position of the mark. If there + * is a selection, the mark will not be the same as + * the dot. + * + * @return the position >=0 + */ + public int getMark() { + return (markPos != null) ? markPos.getOffset() : getDot(); + } + + /** + * Determines if there currently is a selection. + * + * @return true if there's a selection or false if there's no selection for this caret. + */ + public boolean isSelection() { + return (dotPos != null && markPos != null && + markPos != dotPos && dotPos.getOffset() != markPos.getOffset()); + } + + /** + * Determines if the selection is currently visible. + * + * @return true if both {@link #isSelection() } and {@link EditorCaret#isSelectionVisible() } are true. + */ + public boolean isSelectionShowing() { + return caretItem.editorCaret().isSelectionVisible() && isSelection(); + } + + /** + * Returns the selected text's start position. Return 0 for an + * empty document, or the value of dot if no selection. + * + * @return the start position ≥ 0 + */ + public int getSelectionStart() { + return Math.min(getDot(), getMark()); + } + + /** + * Returns the selected text's end position. Return 0 if the document + * is empty, or the value of dot if there is no selection. + * + * @return the end position ≥ 0 + */ + public int getSelectionEnd() { + return Math.max(getDot(), getMark()); + } + + /** + * Gets the current caret visual location. + * + * @return the visual position. + */ + public @CheckForNull Point getMagicCaretPosition() { + return magicCaretPosition; + } + + CaretItem getCaretItem() { + return caretItem; + } + + @Override + public String toString() { + return "dotPos=" + dotPos + ", markPos=" + markPos + ", magicCaretPosition=" + magicCaretPosition + // NOI18N + "\n caretItem=" + caretItem; // NOI18N + } + +} diff --git a/editor.lib2/src/org/netbeans/api/editor/caret/CaretItem.java b/editor.lib2/src/org/netbeans/api/editor/caret/CaretItem.java new file mode 100644 --- /dev/null +++ b/editor.lib2/src/org/netbeans/api/editor/caret/CaretItem.java @@ -0,0 +1,254 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2016 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 2016 Sun Microsystems, Inc. + */ +package org.netbeans.api.editor.caret; + +import java.awt.Point; +import java.awt.Rectangle; +import java.util.logging.Logger; +import javax.swing.text.Position; +import org.netbeans.api.annotations.common.CheckForNull; + +/** + * A single caret inside {@link EditorCaret} handled internally by EditorCaret. + *
    + * For API client methods {@link CaretInfo} class is used. There is one-to-one reference + * between caret item and caret info. But the info is immutable so once the caret item + * gets mutated its corresponding caret info becomes obsolete and new info gets created + * lazily. + * + * @author Miloslav Metelka + */ +final class CaretItem implements Comparable { + + // -J-Dorg.netbeans.modules.editor.lib2.CaretItem.level=FINEST + private static final Logger LOG = Logger.getLogger(CaretItem.class.getName()); + + private static final int TRANSACTION_MARK_REMOVED = 1; + + private static final int UPDATE_VISUAL_BOUNDS = 2; + + private final EditorCaret editorCaret; + + private Position dotPos; + + private Position markPos; + + private Point magicCaretPosition; + + /** + * Last info or null if info became obsolete and should be recomputed. + */ + private CaretInfo info; + + private Rectangle caretBounds; + + /** + * Hint of index of this caret item in replaceCaretItems in transaction context. + */ + private int transactionIndexHint; + + /** + * Transaction uses this flag to mark this item for removal. + */ + private int statusBits; + + CaretItem(EditorCaret editorCaret, Position dotPos, Position markPos) { + this.editorCaret = editorCaret; + this.dotPos = dotPos; + this.markPos = markPos; + this.statusBits = UPDATE_VISUAL_BOUNDS; // Request visual bounds updating automatically + } + + EditorCaret editorCaret() { + return editorCaret; + } + + void ensureValidInfo() { + // No explicit locking - locking managed by EditorCaret + if (info == null) { + info = new CaretInfo(this); + } + } + + void clearInfo() { + // No explicit locking - locking managed by EditorCaret + this.info = null; + } + + CaretInfo getValidInfo() { + ensureValidInfo(); + return info; + } + +// void clearTransactionInfo() { +// // No explicit locking - locking managed by EditorCaret +// this.transactionInfo = null; +// } +// +// CaretInfo getValidTransactionInfo() { +// if (transactionInfo == null) { +// transactionInfo = new CaretInfo(this); +// } +// return transactionInfo; +// } + + /** + * Get position of the caret itself. + * + * @return non-null position of the caret placement. The position may be + * virtual so methods in {@link org.netbeans.api.editor.document.ShiftPositions} may be used if necessary. + */ + @CheckForNull + Position getDotPosition() { + return dotPos; + } + + /** + * Return either the same object like {@link #getDotPosition()} if there's + * no selection or return position denoting the other end of an existing + * selection (which is either before or after the dot position depending of + * how the selection was created). + * + * @return non-null position of the caret placement. The position may be + * virtual so methods in {@link org.netbeans.api.editor.document.ShiftPositions} may be used if necessary. + */ + @CheckForNull + Position getMarkPosition() { + return markPos; + } + + int getDot() { + return (dotPos != null) ? dotPos.getOffset() : 0; + } + + int getMark() { + return (markPos != null) ? markPos.getOffset() : 0; + } + + /** + * @return true if there's a selection or false if there's no selection for + * this caret. + */ + boolean isSelection() { + return (dotPos != null && markPos != null && + markPos != dotPos && dotPos.getOffset() != markPos.getOffset()); + } + + boolean isSelectionShowing() { + return editorCaret.isSelectionVisible() && isSelection(); + } + + Position getSelectionStart() { + return dotPos; // TBD - possibly inspect virtual columns etc. + } + + Position getSelectionEnd() { + return dotPos; // TBD - possibly inspect virtual columns etc. + } + + Point getMagicCaretPosition() { + return magicCaretPosition; + } + + void setDotPos(Position dotPos) { + this.dotPos = dotPos; + } + + void setMarkPos(Position markPos) { + this.markPos = markPos; + } + + void setMagicCaretPosition(Point newMagicCaretPosition) { // [TODO] move to transaction context + this.magicCaretPosition = newMagicCaretPosition; + } + + void setCaretBounds(Rectangle newCaretBounds) { + this.caretBounds = newCaretBounds; + } + + Rectangle getCaretBounds() { + return this.caretBounds; + } + + int getTransactionIndexHint() { + return transactionIndexHint; + } + + void setTransactionIndexHint(int transactionIndexHint) { + this.transactionIndexHint = transactionIndexHint; + } + + void markTransactionMarkRemoved() { + this.statusBits |= TRANSACTION_MARK_REMOVED; + } + + boolean isTransactionMarkRemoved() { + return (this.statusBits & TRANSACTION_MARK_REMOVED) != 0; + } + + void clearTransactionMarkRemoved() { + this.statusBits &= ~TRANSACTION_MARK_REMOVED; + } + + void markUpdateVisualBounds() { + this.statusBits |= UPDATE_VISUAL_BOUNDS; + } + + boolean isUpdateVisualBounds() { + return (this.statusBits & UPDATE_VISUAL_BOUNDS) != 0; + } + + void clearUpdateVisualBounds() { + this.statusBits &= ~UPDATE_VISUAL_BOUNDS; + } + + @Override + public int compareTo(Object o) { + return getDot() - ((CaretItem)o).getDot(); + } + + @Override + public String toString() { + return "dotPos=" + dotPos + ", markPos=" + markPos + ", magicCaretPosition=" + magicCaretPosition; // NOI18N + } + +} diff --git a/editor.lib2/src/org/netbeans/api/editor/caret/CaretMoveContext.java b/editor.lib2/src/org/netbeans/api/editor/caret/CaretMoveContext.java new file mode 100644 --- /dev/null +++ b/editor.lib2/src/org/netbeans/api/editor/caret/CaretMoveContext.java @@ -0,0 +1,172 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2016 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 2016 Sun Microsystems, Inc. + */ +package org.netbeans.api.editor.caret; + +import java.awt.Point; +import java.util.List; +import javax.swing.text.Document; +import javax.swing.text.JTextComponent; +import javax.swing.text.Position; +import org.netbeans.api.annotations.common.NonNull; + +/** + * Context for carets moving within {@link CaretMoveHandler}. + * + * @author Miloslav Metelka + * @since 2.6 + */ +public final class CaretMoveContext { + + private CaretTransaction transaction; + + CaretMoveContext(CaretTransaction transactionContext) { + this.transaction = transactionContext; + } + + /** + * Get list of carets at the time when transaction started. + *
    + * Note: information contained in the returned list will not reflect changes + * performed by the dot/selection modification methods contained in this class. + * It will always return the original state from the beginning of the move transaction. + * All the performed modifications will be incorporated at the end of the move transaction. + * + * @return list of carets at the time when moving transaction has started. + */ + public @NonNull List getOriginalCarets() { + return transaction.getOriginalCarets(); + } + + /** + * Get last item from the list returned by {@link #getOriginalCarets()}. + * @return last caret at the time when caret moving transaction has started. + * @see #getOriginalCarets() + */ + public @NonNull CaretInfo getOriginalLastCaret() { + List origCarets = getOriginalCarets(); + return origCarets.get(origCarets.size() - 1); + } + + /** + * Get list of carets sorted ascending order at the time when transaction started. + *
    + * Note: information contained in the returned list will not reflect changes + * performed by the dot/selection modification methods contained in this class. + * It will always return the original state from the beginning of the move transaction. + * All the performed modifications will be incorporated at the end of the move transaction. + * + * @return list of carets at the time when caret moving transaction has started. + */ + public @NonNull List getOriginalSortedCarets() { + return transaction.getOriginalSortedCarets(); + } + + /** + * Change dot of the given caret. + * + * @param caret non-null caret. + * @param dotPos new dot position. + * @return false if passed caret is obsolete or invalid (e.g. a member of another {@link EditorCaret}) + * or true otherwise. + */ + public boolean setDot(@NonNull CaretInfo caret, @NonNull Position dotPos) { + return setDotAndMark(caret, dotPos, dotPos); + } + + /** + * Move dot of the given getCaret so getCaret selection gets created or changed. + * + * @param caret non-null getCaret. + * @param dotPos new dot position. + * @return false if passed caret is obsolete or invalid (e.g. a member of another {@link EditorCaret}) + * or true otherwise. + */ + public boolean moveDot(@NonNull CaretInfo caret, @NonNull Position dotPos) { + return transaction.moveDot(caret.getCaretItem(), dotPos); + } + + /** + * Move dot of the given getCaret so getCaret selection gets created or changed. + * + * @param caret non-null getCaret. + * @param dotPos new dot position. + * @param markPos starting position of the selection or the same position like dotPos if there should be no selection. + *
    + * The position may point to a lower offset than dotPos in case the selection + * should extend from a higher offset to a lower offset. + * @return false if passed caret is obsolete or invalid (e.g. a member of another {@link EditorCaret}) + * or true otherwise. + */ + public boolean setDotAndMark(@NonNull CaretInfo caret, @NonNull Position dotPos, @NonNull Position markPos) { + return transaction.setDotAndMark(caret.getCaretItem(), dotPos, markPos); + } + + /** + * Set magic caret position of the given caret. + * + * @param caret non-null caret. + * @param p new magic caret position. + * @return false if passed caret is obsolete or invalid (e.g. a member of another {@link EditorCaret}) + * or true otherwise. + */ + public boolean setMagicCaretPosition(@NonNull CaretInfo caret, Point p) { + return transaction.setMagicCaretPosition(caret.getCaretItem(), p); + } + + /** + * Return component in which the caret is currently installed. + * @return non-null component in which the caret is installed. + */ + public @NonNull JTextComponent getComponent() { + return transaction.getComponent(); + } + + /** + * Get document associated with the text component. + * @return document of the associated component. + * @see #getComponent() + */ + public Document getDocument() { + return getComponent().getDocument(); + } + +} diff --git a/editor.lib2/src/org/netbeans/api/editor/caret/CaretTransaction.java b/editor.lib2/src/org/netbeans/api/editor/caret/CaretTransaction.java new file mode 100644 --- /dev/null +++ b/editor.lib2/src/org/netbeans/api/editor/caret/CaretTransaction.java @@ -0,0 +1,627 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2016 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 2016 Sun Microsystems, Inc. + */ +package org.netbeans.api.editor.caret; + +import org.netbeans.spi.editor.caret.CaretMoveHandler; +import java.awt.Point; +import java.util.List; +import javax.swing.text.BadLocationException; +import javax.swing.text.Document; +import javax.swing.text.JTextComponent; +import javax.swing.text.Position; +import org.netbeans.api.annotations.common.NonNull; +import org.netbeans.api.editor.document.ShiftPositions; +import org.netbeans.lib.editor.util.GapList; + +/** + * Context passed to getCaret transaction allowing to create/remove/modify the carets during the transaction. + * + * @author Miloslav Metelka + */ +final class CaretTransaction { + + private final EditorCaret editorCaret; + + private final JTextComponent component; + + private final Document doc; + + /** + * Original items held here mainly due to latter repaints of the removed items. + */ + private final GapList origCaretItems; + + private int modIndex; + + /** + * For MOVE_HANDLER and CARETS_REMOVE this is end index of modified area. + * For CARETS_ADD and MIXED this is not filled. + */ + private int modEndIndex; + + private CaretItem[] addCaretItems; + + private int affectedStartIndex = Integer.MAX_VALUE; + + private int affectedEndIndex; + + private int affectedStartOffset = Integer.MAX_VALUE; + + private int affectedEndOffset; + + private boolean addOrRemove; + + private boolean dotOrMarkChanged; + + private GapList replaceItems; + + private GapList replaceSortedItems; + + private GapList extraRemovedItems; + + private int[] indexes; + + private int indexesLength; + + /** + * End of area where index hints were updated. + */ + private int indexHintEnd; + + private boolean fullResort; + + + CaretTransaction(EditorCaret caret, JTextComponent component, Document doc) { + this.editorCaret = caret; + this.component = component; + this.doc = doc; + this.origCaretItems = editorCaret.getCaretItems(); + } + + + @NonNull EditorCaret getCaret() { + return editorCaret; + } + + @NonNull JTextComponent getComponent() { + return component; + } + + @NonNull Document getDocument() { + return doc; + } + + boolean isAnyChange() { + return addOrRemove || dotOrMarkChanged; + } + + boolean moveDot(@NonNull CaretItem caret, @NonNull Position dotPos) { + return setDotAndMark(caret, dotPos, caret.getMarkPosition()); + } + + boolean setDotAndMark(@NonNull CaretItem caretItem, @NonNull Position dotPos, @NonNull Position markPos) { + assert (dotPos != null) : "dotPos must not be null"; + assert (markPos != null) : "markPos must not be null"; + int index = findCaretItemIndex(origCaretItems, caretItem); + if (index != -1) { + Position origDotPos = caretItem.getDotPosition(); + Position origMarkPos = caretItem.getMarkPosition(); + boolean changed = false; + if (origDotPos == null || ShiftPositions.compare(dotPos, origDotPos) != 0) { + caretItem.setDotPos(dotPos); + changed = true; + } + if (origMarkPos == null || ShiftPositions.compare(markPos, origMarkPos) != 0) { + caretItem.setMarkPos(markPos); + changed = true; + } + if (changed) { + updateAffectedIndexes(index, index + 1); + caretItem.markUpdateVisualBounds(); + caretItem.clearInfo(); + dotOrMarkChanged = true; + } + return changed; + } + return false; + //caret.setDotCaret(offset, this, true); + } + + boolean setMagicCaretPosition(@NonNull CaretItem caretItem, Point p) { + int index = findCaretItemIndex(origCaretItems, caretItem); + if (index != -1) { + caretItem.setMagicCaretPosition(p); + caretItem.clearInfo(); + updateAffectedIndexes(index, index + 1); + return true; + } + return false; + } + + void documentInsertAtZeroOffset(int insertEndOffset) { + // Nested insert inside active transaction for caret moving + // Since carets may already be moved - do the operations with CaretItems directly + Position insertEndPos = null; + for (CaretItem caretItem : editorCaret.getSortedCaretItems()) { + Position dotPos = caretItem.getDotPosition(); + boolean modifyDot = (dotPos == null || dotPos.getOffset() == 0); + Position markPos = caretItem.getMarkPosition(); + boolean modifyMark = (markPos == null || markPos.getOffset() == 0); + if (modifyDot || modifyMark) { + if (insertEndPos == null) { + try { + insertEndPos = doc.createPosition(insertEndOffset); + } catch (BadLocationException ex) { + // Should never happen + return; + } + } + if (modifyDot) { + dotPos = insertEndPos; + } + if (modifyMark) { + markPos = insertEndPos; + } + setDotAndMark(caretItem, dotPos, markPos); + } + // Do not break the loop when caret's pos is above zero offset + // since the carets may be already moved during the transaction + // - possibly to offset zero. But there could be optimization + // at least scan position of only the caret items that were modified and not others. + } + if (insertEndPos != null) { + updateAffectedOffsets(0, insertEndOffset); // TODO isn't this extra work that setDotAndMark() already did?? + fullResort = true; + } + } + + void documentRemove(int offset) { + fullResort = true; // TODO modify to more specific update + } + + private void setSelectionStartEnd(@NonNull CaretItemInfo info, @NonNull Position pos, boolean start) { + int index = findCaretItemIndex(origCaretItems, info.caretItem); + assert (index != -1) : "Index=" + index + " should be valid"; + if (start == info.dotAtStart) { + info.caretItem.setDotPos(pos); + } else { + info.caretItem.setMarkPos(pos); + } + updateAffectedIndexes(index, index + 1); + } + + void handleCaretRemove(@NonNull CaretInfo caret) { + + } + + GapList getReplaceItems() { + return replaceItems; + } + + GapList getSortedCaretItems() { + return replaceSortedItems; + } + + List getOriginalCarets() { + // Current impl ignores possible replaceItems content since client transactions only operate over + // original infos from editorCaret. Internal transactions know the type of transaction + // that was performed so they will skip carets possibly removed by the transaction. + return editorCaret.getCarets(); + } + + List getOriginalSortedCarets() { + // Current impl ignores possible replaceItems content - see getOriginalCarets() + return editorCaret.getSortedCarets(); + } + + void replaceCarets(RemoveType removeType, int offset, CaretItem[] addCaretItems) { + int size = origCaretItems.size(); + switch (removeType) { + case NO_REMOVE: + break; + case REMOVE_LAST_CARET: + if (size > 1) { + modIndex = size - 1; + modEndIndex = size; + addOrRemove = true; + } + break; + case RETAIN_LAST_CARET: + if (size > 1) { + modEndIndex = size - 1; + addOrRemove = true; + } + break; + case REMOVE_ALL_CARETS: + if (size > 0) { + modEndIndex = size; + addOrRemove = true; + } + break; + case DOCUMENT_REMOVE: + break; + case DOCUMENT_INSERT_ZERO_OFFSET: + documentInsertAtZeroOffset(offset); + break; + + default: + throw new AssertionError("Unhandled removeType=" + removeType); // NOI18N + } + if (addCaretItems != null) { + this.addCaretItems = addCaretItems; + addOrRemove = true; + } + } + + void runCaretMoveHandler(CaretMoveHandler handler) { + CaretMoveContext context = new CaretMoveContext(this); + handler.moveCarets(context); + } + + private static int getOffset(Position pos) { + return (pos != null) ? pos.getOffset() : 0; + } + + void removeOverlappingRegions() { + removeOverlappingRegions(0, Integer.MAX_VALUE); + } + + void removeOverlappingRegions(int removeOffset) { + removeOverlappingRegions(0, removeOffset); // TODO compute startIndex by binary search + } + + void removeOverlappingRegions(int startIndex, int stopOffset) { + if (addOrRemove) { + initReplaceItems(); + } else if (dotOrMarkChanged) { + initReplaceItems(); // TODO optimize for low number of changed items + } + GapList origSortedItems = (replaceSortedItems != null) + ? replaceSortedItems + : editorCaret.getSortedCaretItems(); + int origSortedItemsSize = origSortedItems.size(); + GapList nonOverlappingItems = null; + int copyStartIndex = 0; + int i = startIndex - 1; + boolean itemsRemoved = false; + CaretItemInfo lastInfo = new CaretItemInfo(); + if (i >= 0) { + lastInfo.update(origSortedItems.get(i)); + } // Otherwise leave the default zeros in lastInfo + + CaretItemInfo itemInfo = new CaretItemInfo(); + while (++i < origSortedItemsSize) { + itemInfo.update(origSortedItems.get(i)); + if (itemInfo.overlapsAtStart(lastInfo)) { + if (nonOverlappingItems == null) { + nonOverlappingItems = new GapList(origSortedItemsSize - 1); // At least one will be skipped + } + itemsRemoved = true; + // Determine type of overlap + if (!lastInfo.dotAtStart) { // Caret of lastInfo moved into next block + if (lastInfo.startsBelow(itemInfo)) { + // Extend selection of itemInfo to start of lastInfo + updateAffectedOffsets(lastInfo.startOffset, itemInfo.startOffset); + setSelectionStartEnd(itemInfo, lastInfo.startPos, true); + } + // Remove lastInfo's getCaret item + lastInfo.caretItem.markTransactionMarkRemoved(); + origSortedItems.copyElements(copyStartIndex, i - 1, nonOverlappingItems); + copyStartIndex = i; + + } else { // Remove itemInfo and set selection of lastInfo to end of itemInfo + if (itemInfo.endsAbove(lastInfo)) { + updateAffectedOffsets(lastInfo.endOffset, itemInfo.endOffset); + setSelectionStartEnd(lastInfo, itemInfo.endPos, false); + } + // Remove itemInfo's getCaret item + itemInfo.caretItem.markTransactionMarkRemoved(); + origSortedItems.copyElements(copyStartIndex, i, nonOverlappingItems); + copyStartIndex = i + 1; + } + } else { // No overlapping + // Swap the items to reuse original lastInfo + CaretItemInfo tmp = lastInfo; + lastInfo = itemInfo; + itemInfo = tmp; + if (lastInfo.endOffset > stopOffset) { + break; + } + } + } + + if (itemsRemoved) { // At least one item removed + if (copyStartIndex < origSortedItemsSize) { + origSortedItems.copyElements(copyStartIndex, origSortedItemsSize, nonOverlappingItems); + } + GapList origItems = resultItems(); + int origItemsSize = origItems.size(); + replaceItems = new GapList<>(origItemsSize); + for (i = 0; i < origItemsSize; i++) { + CaretItem caretItem = origItems.get(i); + if (caretItem.isTransactionMarkRemoved()) { + caretItem.clearTransactionMarkRemoved(); + if (extraRemovedItems == null) { + extraRemovedItems = new GapList<>(); + } + extraRemovedItems.add(caretItem); + } else { + replaceItems.add(caretItem); + } + } + replaceSortedItems = nonOverlappingItems; + } + } + + GapList addRemovedItems(GapList toItems) { + int removeSize = modEndIndex - modIndex; + int extraRemovedSize = (extraRemovedItems != null) ? extraRemovedItems.size() : 0; + if (removeSize + extraRemovedSize > 0) { + if (toItems == null) { + toItems = new GapList<>(removeSize + extraRemovedSize); + } + if (removeSize > 0) { + toItems.addAll(origCaretItems, modIndex, removeSize); + } + if (extraRemovedSize > 0) { + toItems.addAll(extraRemovedItems); + } + } + return toItems; + } + + GapList addUpdateVisualBoundsItems(GapList toItems) { + GapList items = resultItems(); + int size = items.size(); + for (int i = 0; i < size; i++) { + CaretItem caretItem = items.get(i); + if (caretItem.isUpdateVisualBounds()) { + caretItem.clearUpdateVisualBounds(); + if (toItems == null) { + toItems = new GapList<>(); + } + toItems.add(caretItem); + } + } + return toItems; + } + + private GapList resultItems() { + return (replaceItems != null) ? replaceItems : origCaretItems; + } + + private void initReplaceItems() { + assert (replaceItems == null) : "replaceItems already inited to " + replaceItems; // NOI18N + int size = origCaretItems.size(); + int removeSize = modEndIndex - modIndex; + int addSize = (addCaretItems != null) ? addCaretItems.length : 0; + int newSize = size - removeSize + addSize; + replaceItems = new GapList<>(newSize); + if (removeSize > 0) { + replaceItems.addAll(origCaretItems, 0, modIndex); + replaceItems.addAll(origCaretItems, modEndIndex, size - modEndIndex); + } else { + replaceItems.addAll(origCaretItems); + } + if (addCaretItems != null) { + replaceItems.addArray(replaceItems.size(), addCaretItems); + } + + assert (replaceItems.size() == newSize); + boolean updateIndividual = (removeSize + addSize) < (newSize >> 2); // Threshold 1/4 of total size for full resort + if (fullResort || true) { // Force full resort + replaceSortedItems = replaceItems.copy(); + if (newSize > 1) { + + } + } else { // Partial resort TODO + + } + } + + private void resetIndexes() { + indexesLength = 0; + } + + private void addToIndexes(int index) { + if (indexes == null) { + indexes = new int[8]; + } else if (indexesLength == indexes.length) { + int[] orig = indexes; + indexes = new int[indexesLength << 1]; + System.arraycopy(orig, 0, indexes, 0, indexesLength); + } + indexes[indexesLength++] = index; + } + + + private int findCaretItemIndex(GapList caretItems, CaretItem caretItem) { + // Method only resolves existing items not added items + int i = caretItem.getTransactionIndexHint(); + int size = caretItems.size(); + if (i >= size || caretItems.get(i) != caretItem) { + while (indexHintEnd < size) { + CaretItem c = caretItems.get(indexHintEnd); + c.setTransactionIndexHint(indexHintEnd++); + if (c == caretItem) { + return indexHintEnd - 1; + } + } + return -1; + } + return i; + } + + private void updateAffectedIndexes(int startIndex, int endIndex) { + if (affectedStartIndex == Integer.MAX_VALUE) { + affectedStartIndex = startIndex; + affectedEndIndex = endIndex; + } else { + affectedStartIndex = Math.min(affectedStartIndex, startIndex); + affectedEndIndex = Math.max(affectedEndIndex, endIndex); + } + } + + private void updateAffectedOffsets(int startOffset, int endOffset) { + if (affectedStartOffset == Integer.MAX_VALUE) { // Affected range not inited yet + affectedStartOffset = startOffset; + affectedEndOffset = endOffset; + } else { // Affected range already inited + if (startOffset < affectedStartOffset) { + affectedStartOffset = startOffset; + } + if (endOffset > affectedEndOffset) { + affectedEndOffset = endOffset; + } + } + } + + static CaretItem[] asCaretItems(EditorCaret caret, @NonNull List dotAndSelectionStartPosPairs) { + int size = dotAndSelectionStartPosPairs.size(); + if ((size & 1) != 0) { + throw new IllegalStateException("Passed list has size=" + size + " which is not an even number."); + } + CaretItem[] addedCarets = new CaretItem[size >> 1]; + int listIndex = 0; + for (int j = 0; j < addedCarets.length; j++) { + Position dotPos = dotAndSelectionStartPosPairs.get(listIndex++); + Position selectionStartPos = dotAndSelectionStartPosPairs.get(listIndex++); + CaretItem caretItem = new CaretItem(caret, dotPos, selectionStartPos); + addedCarets[j] = caretItem; + } + return addedCarets; + } + + enum RemoveType { + NO_REMOVE, + REMOVE_LAST_CARET, + RETAIN_LAST_CARET, + REMOVE_ALL_CARETS, + DOCUMENT_REMOVE, + DOCUMENT_INSERT_ZERO_OFFSET + } + + /** + * Helper class for resolving overlapping getCaret selections. + */ + private static final class CaretItemInfo { + + CaretItem caretItem; + + Position startPos; + + Position endPos; + + int startOffset; + + int startShift; + + int endOffset; + + int endShift; + + boolean dotAtStart; + + void update(CaretItem caret) { + this.caretItem = caret; + Position dotPos = caret.getDotPosition(); + if (dotPos != null) { + int dotOffset = dotPos.getOffset(); + int dotShift = ShiftPositions.getShift(dotPos); + Position markPos = caret.getMarkPosition(); + if (markPos != null && markPos != dotPos) { // Still they may be equal which means no selection + int markOffset = markPos.getOffset(); + int markShift = ShiftPositions.getShift(markPos); + if (markOffset < dotOffset || (markOffset == dotOffset && markShift <= dotShift)) { + startPos = markPos; + endPos = dotPos; + startOffset = markOffset; + startShift = markShift; + endOffset = dotOffset; + endShift = dotShift; + dotAtStart = false; + } else { + startPos = dotPos; + endPos = markPos; + startOffset = dotOffset; + startShift = dotShift; + endOffset = markOffset; + endShift = markShift; + dotAtStart = true; + } + } else { + startPos = endPos = dotPos; + startOffset = endOffset = dotOffset; + startShift = startShift = dotShift; + dotAtStart = false; + } + } else { + clear(); + } + } + + private void clear() { + caretItem = null; + startPos = endPos = null; + startOffset = endOffset = 0; + startShift = startShift = 0; + dotAtStart = false; + } + + private boolean overlapsAtStart(CaretItemInfo info) { + return (ShiftPositions.compare(info.endOffset, info.endShift, + startOffset, startShift) > 0); + } + + private boolean startsBelow(CaretItemInfo info) { + return (ShiftPositions.compare(startOffset, startShift, + info.startOffset, info.startShift) < 0); + } + + private boolean endsAbove(CaretItemInfo info) { + return (ShiftPositions.compare(endOffset, endShift, + info.endOffset, info.endShift) > 0); + } + + } + +} diff --git a/editor.lib2/src/org/netbeans/api/editor/caret/EditorCaret.java b/editor.lib2/src/org/netbeans/api/editor/caret/EditorCaret.java new file mode 100644 --- /dev/null +++ b/editor.lib2/src/org/netbeans/api/editor/caret/EditorCaret.java @@ -0,0 +1,2585 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2015 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 2015 Sun Microsystems, Inc. + */ + package org.netbeans.api.editor.caret; + +import org.netbeans.spi.editor.caret.CaretMoveHandler; +import java.awt.AlphaComposite; +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Component; +import java.awt.Composite; +import java.awt.Container; +import java.awt.Cursor; +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.Stroke; +import java.awt.datatransfer.Clipboard; +import java.awt.datatransfer.DataFlavor; +import java.awt.datatransfer.Transferable; +import java.awt.datatransfer.UnsupportedFlavorException; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.ComponentAdapter; +import java.awt.event.ComponentEvent; +import java.awt.event.ComponentListener; +import java.awt.event.FocusEvent; +import java.awt.event.FocusListener; +import java.awt.event.InputEvent; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.awt.event.MouseMotionListener; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +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; +import java.util.prefs.PreferenceChangeListener; +import java.util.prefs.Preferences; +import javax.swing.Action; +import javax.swing.JComponent; +import javax.swing.JScrollBar; +import javax.swing.JScrollPane; +import javax.swing.JViewport; +import javax.swing.SwingUtilities; +import javax.swing.Timer; +import javax.swing.TransferHandler; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +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.Caret; +import javax.swing.text.DefaultEditorKit; +import javax.swing.text.Document; +import javax.swing.text.Element; +import javax.swing.text.JTextComponent; +import javax.swing.text.Position; +import javax.swing.text.StyleConstants; +import org.netbeans.api.annotations.common.CheckForNull; +import org.netbeans.api.annotations.common.NonNull; +import org.netbeans.api.editor.EditorUtilities; +import org.netbeans.api.editor.document.AtomicLockDocument; +import org.netbeans.api.editor.document.AtomicLockEvent; +import org.netbeans.api.editor.document.AtomicLockListener; +import org.netbeans.api.editor.document.LineDocument; +import org.netbeans.api.editor.document.LineDocumentUtils; +import org.netbeans.api.editor.mimelookup.MimeLookup; +import org.netbeans.api.editor.settings.FontColorNames; +import org.netbeans.api.editor.settings.FontColorSettings; +import org.netbeans.api.editor.settings.SimpleValueNames; +import org.netbeans.lib.editor.util.ArrayUtilities; +import org.netbeans.lib.editor.util.GapList; +import org.netbeans.lib.editor.util.ListenerList; +import org.netbeans.lib.editor.util.swing.DocumentListenerPriority; +import org.netbeans.lib.editor.util.swing.DocumentUtilities; +import org.netbeans.modules.editor.lib2.EditorPreferencesDefaults; +import org.netbeans.modules.editor.lib2.RectangularSelectionCaretAccessor; +import org.netbeans.modules.editor.lib2.RectangularSelectionTransferHandler; +import org.netbeans.modules.editor.lib2.RectangularSelectionUtils; +import org.netbeans.modules.editor.lib2.actions.EditorActionUtilities; +import org.netbeans.modules.editor.lib2.highlighting.CaretOverwriteModeHighlighting; +import org.netbeans.modules.editor.lib2.view.DocumentView; +import org.netbeans.modules.editor.lib2.view.LockedViewHierarchy; +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.netbeans.modules.editor.lib2.view.ViewUtils; +import org.openide.util.Exceptions; +import org.openide.util.WeakListeners; + +/** + * Extension to standard Swing caret used by all NetBeans editors. + *
    + * It supports multi-caret editing mode where an arbitrary number of carets + * is placed at arbitrary positions throughout a document. + * In this mode each caret is described by its CaretInfo object. + *
    + * The caret works over text components having {@link AbstractDocument} based document. + * + * @author Miloslav Metelka + * @author Ralph Ruijs + * @since 2.6 + */ +public final class EditorCaret implements Caret { + + // Temporary until rectangular selection gets ported to multi-caret support + private static final String RECTANGULAR_SELECTION_PROPERTY = "rectangular-selection"; // NOI18N + private static final String RECTANGULAR_SELECTION_REGIONS_PROPERTY = "rectangular-selection-regions"; // NOI18N + + // -J-Dorg.netbeans.editor.BaseCaret.level=FINEST + private static final Logger LOG = Logger.getLogger(EditorCaret.class.getName()); + + static { + RectangularSelectionCaretAccessor.register(new RectangularSelectionCaretAccessor() { + @Override + public void setRectangularSelectionToDotAndMark(EditorCaret editorCaret) { + editorCaret.setRectangularSelectionToDotAndMark(); + } + + @Override + public void updateRectangularUpDownSelection(EditorCaret editorCaret) { + editorCaret.updateRectangularUpDownSelection(); + } + + @Override + public void extendRectangularSelection(EditorCaret editorCaret, boolean toRight, boolean ctrl) { + editorCaret.extendRectangularSelection(toRight, ctrl); + } + }); + } + + static final long serialVersionUID = 0L; + + /** + * Non-empty list of individual carets in the order they were created. + * At least one item is always present. + */ + private @NonNull GapList caretItems; + + /** + * Non-empty list of individual carets in the order they were created. + * At least one item is always present. + */ + private @NonNull GapList sortedCaretItems; + + /** + * Cached infos corresponding to caret items or null if any of the items were changed + * so another copy of the caret items- (translated into caret infos) will get created + * upon query to {@link #getCarets() }. + *
    + * Once the list gets created both the list content and information in each caret info are immutable. + */ + private List caretInfos; + + /** + * Cached infos corresponding to sorted caret items or null if any of the sorted items were changed + * so another copy of the sorted caret items (translated into caret infos) will get created + * upon query to {@link #getSortedCarets() }. + *
    + * Once the list gets created both the list content and information in each caret info are immutable. + */ + private List sortedCaretInfos; + + /** Component this caret is bound to */ + private JTextComponent component; + + /** List of individual carets */ + private boolean overwriteMode; + + private final ListenerList listenerList; + + private final ListenerList changeListenerList; + + /** + * Implementor of various listeners. + */ + private final ListenerImpl listenerImpl; + + /** Is the caret visible (after setVisible(true) call? */ + private boolean visible; + + /** + * Whether blinking caret is currently visible on the screen. + *
    + * This changes from true to false after each tick of a timer + * (assuming visible == true). + */ + private boolean blinkVisible; + + /** + * Determine if a possible selection would be displayed or not. + */ + private boolean selectionVisible; + + /** Type of the caret */ + private CaretType type = CaretType.THICK_LINE_CARET; + + /** Width of caret */ + private int thickCaretWidth = EditorPreferencesDefaults.defaultThickCaretWidth; + + private MouseState mouseState = MouseState.DEFAULT; + + /** Timer used for blinking the caret */ + private Timer flasher; + + private Action selectWordAction; + private Action selectLineAction; + + private AbstractDocument activeDoc; + + private Thread lockThread; + + private int lockDepth; + + private CaretTransaction activeTransaction; + + /** + * Items from previous transaction(s) that need their visual rectangle + * to get repainted to clear the previous caret representation visually. + */ + private GapList pendingRepaintRemovedItemsList; + + /** + * Items from previous transaction(s) that need their visual bounds + * to be recomputed and caret to be repainted then. + */ + private GapList pendingUpdateVisualBoundsItemsList; + + /** + * Caret item to which the view should scroll or null for no scrolling. + */ + private CaretItem scrollToItem; + + /** + * Whether the text is being modified under atomic lock. + * If so just one caret change is fired at the end of all modifications. + */ + private transient boolean inAtomicLock = false; + private transient boolean inAtomicUnlock = false; + + /** + * Helps to check whether there was modification performed + * and so the caret change needs to be fired. + */ + private transient boolean modified; + + /** + * Set to true once the folds have changed. The caret should retain + * its relative visual position on the screen. + */ + private boolean updateAfterFoldHierarchyChange; + + /** + * Whether at least one typing change occurred during possibly several atomic operations. + */ + private boolean typingModificationOccurred; + + private Preferences prefs = null; + + private PreferenceChangeListener weakPrefsListener = null; + + private boolean caretUpdatePending; + + /** + * Minimum selection start for word and line selections. + * This helps to ensure that when extending word (or line) selections + * the selection will always include at least the initially selected word (or line). + */ + private int minSelectionStartOffset; + + private int minSelectionEndOffset; + + private boolean rectangularSelection; + + /** + * Rectangle that corresponds to model2View of current point of selection. + */ + private Rectangle rsDotRect; + + /** + * Rectangle that corresponds to model2View of beginning of selection. + */ + private Rectangle rsMarkRect; + + /** + * Rectangle marking rectangular selection. + */ + private Rectangle rsPaintRect; + + /** + * List of start-pos and end-pos pairs that denote rectangular selection + * on the selected lines. + */ + private List rsRegions; + + /** + * Used for showing the default cursor instead of the text cursor when the + * mouse is over a block of selected text. + * This field is used to prevent repeated calls to component.setCursor() + * with the same cursor. + */ + private boolean showingTextCursor = true; + + public EditorCaret() { + caretItems = new GapList<>(); + sortedCaretItems = new GapList<>(); + CaretItem singleCaret = new CaretItem(this, null, null); + caretItems.add(singleCaret); + sortedCaretItems.add(singleCaret); + + listenerList = new ListenerList<>(); + changeListenerList = new ListenerList<>(); + listenerImpl = new ListenerImpl(); + } + + @Override + public int getDot() { + return getLastCaret().getDot(); + } + + @Override + public int getMark() { + return getLastCaret().getMark(); + } + + /** + * Get information about all existing carets in the order they were created. + *
    + * The list always has at least one item. The last caret (last item of the list) + * is the most recent caret. + *
    + * The list is a snapshot of the current state of the carets. The list content itself and its contained + * caret infos are guaranteed not change after subsequent calls to caret API or document modifications. + *
    + * The list is nonmodifiable. + *
    + * This method should be called with document's read-lock acquired which will guarantee + * stability of {@link CaretInfo#getDot() } and {@link CaretInfo#getMark() } and prevent + * caret merging as a possible effect of document modifications. + * + * @return copy of caret list with size >= 1 containing information about all carets. + */ + public @NonNull List getCarets() { + synchronized (listenerList) { + if (caretInfos == null) { + int i = caretItems.size(); + CaretInfo[] infos = new CaretInfo[i--]; + for (; i >= 0; i--) { + infos[i] = caretItems.get(i).getValidInfo(); + } + caretInfos = ArrayUtilities.unmodifiableList(infos); + } + return caretInfos; + } + } + + /** + * Get information about all existing carets sorted by dot positions in ascending order. + *
    + * The list is a snapshot of the current state of the carets. The list content itself and its contained + * caret infos are guaranteed not change after subsequent calls to caret API or document modifications. + *
    + * The list is nonmodifiable. + *
    + * This method should be called with document's read-lock acquired which will guarantee + * stability of {@link CaretInfo#getDot() } and {@link CaretInfo#getMark() } and prevent + * caret merging as a possible effect of document modifications. + * + * @return copy of caret list with size >= 1 sorted by dot positions in ascending order. + */ + public @NonNull List getSortedCarets() { + synchronized (listenerList) { + if (sortedCaretInfos == null) { + int i = sortedCaretItems.size(); + CaretInfo[] sortedInfos = new CaretInfo[i--]; + for (; i >= 0; i--) { + sortedInfos[i] = sortedCaretItems.get(i).getValidInfo(); + } + sortedCaretInfos = ArrayUtilities.unmodifiableList(sortedInfos); + } + return sortedCaretInfos; + } + } + + /** + * Get info about the most recently created caret. + *
    + * For normal mode this is the only caret returned by {@link #getCarets() }. + *
    + * For multi-caret mode this is the last item in the list returned by {@link #getCarets() }. + * + * @return last caret (the most recently added caret). + */ + public @NonNull CaretInfo getLastCaret() { + synchronized (listenerList) { + return caretItems.get(caretItems.size() - 1).getValidInfo(); + } + } + + /** + * Get information about the caret at the specified offset. + * + * @param offset the offset of the caret + * @return CaretInfo for the caret at offset, null if there is no caret or + * the offset is invalid + */ + public @CheckForNull CaretInfo getCaretAt(int offset) { + return null; // TBD + } + + /** + * Assign a new offset to the caret in the underlying document. + *
    + * This method implicitly sets the selection range to zero. + *
    + * If multiple carets are present this method retains only last caret + * which then moves to the given offset. + * + * @param offset {@inheritDoc} + * @see Caret#setDot(int) + */ + public @Override void setDot(final int offset) { + if (LOG.isLoggable(Level.FINE)) { + LOG.fine("setDot: offset=" + offset); //NOI18N + if (LOG.isLoggable(Level.FINEST)) { + LOG.log(Level.INFO, "setDot call stack", new Exception()); + } + } + runTransaction(CaretTransaction.RemoveType.RETAIN_LAST_CARET, 0, null, new CaretMoveHandler() { + @Override + public void moveCarets(CaretMoveContext context) { + Document doc = context.getComponent().getDocument(); + if (doc != null) { + try { + Position pos = doc.createPosition(offset); + context.setDot(context.getOriginalLastCaret(), pos); + } catch (BadLocationException ex) { + // Ignore the setDot() request + } + } + } + }); + } + + /** + * Moves the caret position (dot) to some other position, leaving behind the + * mark. This is useful for making selections. + *
    + * If multiple carets are present this method retains all carets + * and moves the dot of the last caret to the given offset. + * + * @param offset {@inheritDoc} + * @see Caret#moveDot(int) + */ + public @Override void moveDot(final int offset) { + if (LOG.isLoggable(Level.FINE)) { + LOG.fine("moveDot: offset=" + offset); //NOI18N + } + + runTransaction(CaretTransaction.RemoveType.NO_REMOVE, 0, null, new CaretMoveHandler() { + @Override + public void moveCarets(CaretMoveContext context) { + Document doc = context.getComponent().getDocument(); + if (doc != null) { + try { + Position pos = doc.createPosition(offset); + context.moveDot(context.getOriginalLastCaret(), pos); + } catch (BadLocationException ex) { + // Ignore the setDot() request + } + } + } + }); + } + + /** + * Move multiple carets or create/modify selections. + *
    + * For performance reasons this is made as a single transaction over the caret + * with only one change event being fired. + *
    + * Note that the move handler does not permit to add or remove carets - this has to be performed + * by other methods present in this class (as another transaction over the editor caret). + *
    + *
    +     * 
    +     *   // Go one line up with all carets
    +     *   editorCaret.moveCarets(new CaretMoveHandler() {
    +     *     @Override public void moveCarets(CaretMoveContext context) {
    +     *       for (CaretInfo caretInfo : context.getOriginalSortedCarets()) {
    +     *         try {
    +     *           int dot = caretInfo.getDot();
    +     *           dot = Utilities.getPositionAbove(target, dot, p.x);
    +     *           Position dotPos = doc.createPosition(dot);
    +     *           context.setDot(caretInfo, dotPos);
    +     *         } catch (BadLocationException e) {
    +     *           // the position stays the same
    +     *         }
    +     *       }
    +     *     }
    +     *   });
    +     * 
    +     * 
    + * + * @param moveHandler non-null move handler to perform the changes. The handler's methods + * will be given a context to operate on. + * @return difference between current count of carets and the number of carets when the operation started. + * Returns Integer.MIN_VALUE if the operation was cancelled due to the caret not being installed in any text component + * or no document installed in the text component. + */ + public int moveCarets(@NonNull CaretMoveHandler moveHandler) { + return runTransaction(CaretTransaction.RemoveType.NO_REMOVE, 0, null, moveHandler); + } + + /** + * Create a new caret at the given position with a possible selection. + *
    + * The caret will become the last caret of the list returned by {@link #getCarets() }. + *
    + * This method requires the caller to have either read lock or write lock acquired + * over the underlying document. + *
    + *
    +     * 
    +     *   editorCaret.addCaret(pos, pos); // Add a new caret at pos.getOffset()
    +     * 
    +     *   Position pos2 = doc.createPosition(pos.getOffset() + 2);
    +     *   // Add a new caret with selection starting at pos and extending to pos2 with caret located at pos2
    +     *   editorCaret.addCaret(pos2, pos);
    +     *   // Add a new caret with selection starting at pos and extending to pos2 with caret located at pos
    +     *   editorCaret.addCaret(pos, pos2);
    +     * 
    +     * 
    + * + * @param dotPos position of the newly created caret. + * @param markPos beginning of the selection (the other end is dotPos) or the same position like dotPos for no selection. + * The markPos may have higher offset than dotPos to select in a backward direction. + * @return difference between current count of carets and the number of carets when the operation started. + * Returns Integer.MIN_VALUE if the operation was cancelled due to the caret not being installed in any text component + * or no document installed in the text component. + *
    + * Note that adding a new caret to offset where another caret is already located may lead + * to its immediate removal. + */ + public int addCaret(@NonNull Position dotPos, @NonNull Position markPos) { + return runTransaction(CaretTransaction.RemoveType.NO_REMOVE, 0, + new CaretItem[] { new CaretItem(this, dotPos, markPos) }, null); + } + + /** + * Add multiple carets at once. + *
    + * It is similar to calling {@link #addCaret(javax.swing.text.Position, javax.swing.text.Position) } + * multiple times but this method is more efficient (it only fires caret change once). + *
    + * This method requires the caller to have either read lock or write lock acquired + * over the underlying document. + *
    + *
    +     * 
    +     *   List<Position> pairs = new ArrayList<>();
    +     *   pairs.add(dotPos);
    +     *   pairs.add(dotPos);
    +     *   pairs.add(dot2Pos);
    +     *   pairs.add(mark2Pos);
    +     *   // Add caret located at dotPos.getOffset() and another one with selection
    +     *   // starting at mark2Pos and extending to dot2Pos with caret located at dot2Pos
    +     *   editorCaret.addCaret(pairs);
    +     * 
    +     * 
    + * + * @param dotAndMarkPosPairs list of position pairs consisting of dot position + * and mark position (selection start position) which may be the same position like the dot + * if the particular caret has no selection. The list must have even size. + * @return difference between current count of carets and the number of carets when the operation started. + * Returns Integer.MIN_VALUE if the operation was cancelled due to the caret not being installed in any text component + * or no document installed in the text component. + */ + public int addCarets(@NonNull List dotAndMarkPosPairs) { + return runTransaction(CaretTransaction.RemoveType.NO_REMOVE, 0, + CaretTransaction.asCaretItems(this, dotAndMarkPosPairs), null); + } + + /** + * Replace all current carets with the new ones. + *
    + * This method requires the caller to have either read lock or write lock acquired + * over the underlying document. + *
    + * @param dotAndMarkPosPairs list of position pairs consisting of dot position + * and mark position (selection start position) which may be the same position like dot + * if the particular caret has no selection. The list must have even size. + * @return difference between current count of carets and the number of carets when the operation started. + * Returns Integer.MIN_VALUE if the operation was cancelled due to the caret not being installed in any text component + * or no document installed in the text component. + */ + public int replaceCarets(@NonNull List dotAndMarkPosPairs) { + if (dotAndMarkPosPairs.isEmpty()) { + throw new IllegalArgumentException("dotAndSelectionStartPosPairs list must not be empty"); + } + CaretItem[] addedItems = CaretTransaction.asCaretItems(this, dotAndMarkPosPairs); + return runTransaction(CaretTransaction.RemoveType.REMOVE_ALL_CARETS, 0, addedItems, null); + } + + /** + * Remove last added caret (determined by {@link #getLastCaret() }). + *
    + * If there is just one caret the method has no effect. + * + * @return difference between current count of carets and the number of carets when the operation started. + * Returns Integer.MIN_VALUE if the operation was cancelled due to the caret not being installed in any text component + * or no document installed in the text component. + */ + public int removeLastCaret() { + return runTransaction(CaretTransaction.RemoveType.REMOVE_LAST_CARET, 0, null, null); + } + + /** + * Switch to single caret mode by removing all carets except the last caret. + * @return difference between current count of carets and the number of carets when the operation started. + * Returns Integer.MIN_VALUE if the operation was cancelled due to the caret not being installed in any text component + * or no document installed in the text component. + */ + public int retainLastCaretOnly() { + return runTransaction(CaretTransaction.RemoveType.RETAIN_LAST_CARET, 0, null, null); + } + + /** + * Adds listener to track caret changes in detail. + * + * @param listener non-null listener. + */ + public void addEditorCaretListener(@NonNull EditorCaretListener listener) { + listenerList.add(listener); + } + + /** + * Adds listener to track caret position changes (to fulfil {@link Caret} interface). + */ + @Override + public void addChangeListener(@NonNull ChangeListener l) { + changeListenerList.add(l); + } + + public void removeEditorCaretListener(@NonNull EditorCaretListener listener) { + listenerList.remove(listener); + } + + /** + * Removes listener to track caret position changes (to fulfil {@link Caret} interface). + */ + @Override + public void removeChangeListener(@NonNull ChangeListener l) { + changeListenerList.remove(l); + } + + /** + * Determines if the caret is currently visible (it may be blinking depending on settings). + *

    + * Caret becomes visible after setVisible(true) gets called on it. + * + * @return true if visible else false + */ + @Override + public boolean isVisible() { + synchronized (listenerList) { + return visible; + } + } + + /** + * Sets the caret visibility, and repaints the caret. + * + * @param visible the visibility specifier + * @see Caret#setVisible + */ + @Override + public void setVisible(boolean visible) { + if (LOG.isLoggable(Level.FINER)) { + LOG.finer("BaseCaret.setVisible(" + visible + ")\n"); + if (LOG.isLoggable(Level.FINEST)) { + LOG.log(Level.INFO, "", new Exception()); + } + } + synchronized (listenerList) { + if (flasher != null) { + if (this.visible) { + flasher.stop(); + } + if (LOG.isLoggable(Level.FINER)) { + LOG.finer((visible ? "Starting" : "Stopping") + // NOI18N + " the caret blinking timer: " + dumpVisibility() + '\n'); // NOI18N + } + this.visible = visible; + if (visible) { + flasher.start(); + } else { + flasher.stop(); + } + } + } + JTextComponent c = component; + if (c != null) { + // TODO only paint carets showing on screen + List sortedCarets = getSortedCarets(); + for (CaretInfo caret : sortedCarets) { + CaretItem caretItem = caret.getCaretItem(); + if (caretItem.getCaretBounds() != null) { + Rectangle repaintRect = caretItem.getCaretBounds(); + c.repaint(repaintRect); + } + } + } + } + + @Override + public boolean isSelectionVisible() { + return selectionVisible; + } + + @Override + public void setSelectionVisible(boolean v) { + if (selectionVisible == v) { + return; + } + JTextComponent c = component; + Document doc; + if (c != null && (doc = c.getDocument()) != null) { + selectionVisible = v; + // [TODO] ensure to repaint + } + } + + @Override + public void install(JTextComponent c) { + assert (SwingUtilities.isEventDispatchThread()); // must be done in AWT + if (LOG.isLoggable(Level.FINE)) { + LOG.fine("Installing to " + s2s(c)); //NOI18N + } + + component = c; + visible = true; + modelChanged(null, c.getDocument()); + + Boolean b = (Boolean) c.getClientProperty(EditorUtilities.CARET_OVERWRITE_MODE_PROPERTY); + overwriteMode = (b != null) ? b : false; + updateOverwriteModeLayer(true); + setBlinkVisible(true); + + // Attempt to assign initial bounds - usually here the component + // is not yet added to the component hierarchy. + updateAllCaretsBounds(); + + if(getLastCaretItem().getCaretBounds() == null) { + // For null bounds wait for the component to get resized + // and attempt to recompute bounds then + component.addComponentListener(listenerImpl); + } + + component.addPropertyChangeListener(listenerImpl); + component.addFocusListener(listenerImpl); + component.addMouseListener(listenerImpl); + component.addMouseMotionListener(listenerImpl); + component.addKeyListener(listenerImpl); + ViewHierarchy.get(component).addViewHierarchyListener(listenerImpl); + + if (component.hasFocus()) { + if (LOG.isLoggable(Level.FINE)) { + LOG.fine("Component has focus, calling BaseCaret.focusGained(); doc=" // NOI18N + + component.getDocument().getProperty(Document.TitleProperty) + '\n'); + } + listenerImpl.focusGained(null); // emulate focus gained + } + + dispatchUpdate(false); + } + + @Override + public void deinstall(JTextComponent c) { + if (LOG.isLoggable(Level.FINE)) { + LOG.fine("Deinstalling from " + s2s(c)); //NOI18N + } + + synchronized (listenerList) { + if (flasher != null) { + setBlinkRate(0); + } + } + + c.removeComponentListener(listenerImpl); + c.removePropertyChangeListener(listenerImpl); + c.removeFocusListener(listenerImpl); + c.removeMouseListener(listenerImpl); + c.removeMouseMotionListener(listenerImpl); + ViewHierarchy.get(c).removeViewHierarchyListener(listenerImpl); + + + modelChanged(activeDoc, null); + } + + @Override + public void paint(Graphics g) { + JTextComponent c = component; + if (c == null) return; + + // Check whether the caret was moved but the component was not + // validated yet and therefore the caret bounds are still null + // and if so compute the bounds and scroll the view if necessary. + // TODO - could this be done by an extra flag rather than bounds checking?? + CaretItem lastCaret = getLastCaretItem(); + if (getDot() != 0 && lastCaret.getCaretBounds() == null) { + update(true); + } + + List carets = getSortedCarets(); + for (CaretInfo caretInfo : carets) { // TODO only paint the items in the clipped area - use binary search to located first item + CaretItem caretItem = caretInfo.getCaretItem(); + if (LOG.isLoggable(Level.FINEST)) { + LOG.finest("BaseCaret.paint(): caretBounds=" + caretItem.getCaretBounds() + dumpVisibility() + '\n'); + } + if (caretItem.getCaretBounds() != null && isVisible() && blinkVisible) { + paintCaret(g, caretItem); + } + if (rectangularSelection && rsPaintRect != null && g instanceof Graphics2D) { + Graphics2D g2d = (Graphics2D) g; + Stroke stroke = new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 0, new float[] {4, 2}, 0); + Stroke origStroke = g2d.getStroke(); + Color origColor = g2d.getColor(); + try { + // Render translucent rectangle + Color selColor = c.getSelectionColor(); + g2d.setColor(selColor); + Composite origComposite = g2d.getComposite(); + try { + g2d.setComposite(AlphaComposite.SrcOver.derive(0.2f)); + g2d.fill(rsPaintRect); + } finally { + g2d.setComposite(origComposite); + } + // Paint stroked line around rectangular selection rectangle + g.setColor(c.getCaretColor()); + g2d.setStroke(stroke); + Rectangle onePointSmallerRect = new Rectangle(rsPaintRect); + onePointSmallerRect.width--; + onePointSmallerRect.height--; + g2d.draw(onePointSmallerRect); + + } finally { + g2d.setStroke(origStroke); + g2d.setColor(origColor); + } + } + } + } + + public @Override void setMagicCaretPosition(Point p) { + getLastCaretItem().setMagicCaretPosition(p); + } + + public @Override final Point getMagicCaretPosition() { + return getLastCaretItem().getMagicCaretPosition(); + } + + public @Override void setBlinkRate(int rate) { + if (LOG.isLoggable(Level.FINER)) { + LOG.finer("setBlinkRate(" + rate + ")" + dumpVisibility() + '\n'); // NOI18N + } + synchronized (listenerList) { + if (flasher == null && rate > 0) { + flasher = new Timer(rate, listenerImpl); + } + if (flasher != null) { + if (rate > 0) { + if (flasher.getDelay() != rate) { + flasher.setDelay(rate); + } + } else { // zero rate - don't blink + flasher.stop(); + flasher.removeActionListener(listenerImpl); + flasher = null; + setBlinkVisible(true); + if (LOG.isLoggable(Level.FINER)){ + LOG.finer("Zero blink rate - no blinking. flasher=null; blinkVisible=true"); // NOI18N + } + } + } + } + } + + @Override + public int getBlinkRate() { + synchronized (listenerList) { + return (flasher != null) ? flasher.getDelay() : 0; + } + } + + /** + * + */ + void setRectangularSelectionToDotAndMark() { + int dotOffset = getDot(); + int markOffset = getMark(); + try { + rsDotRect = component.modelToView(dotOffset); + rsMarkRect = component.modelToView(markOffset); + } catch (BadLocationException ex) { + rsDotRect = rsMarkRect = null; + } + updateRectangularSelectionPaintRect(); + } + + /** + * + */ + void updateRectangularUpDownSelection() { + JTextComponent c = component; + int dotOffset = getDot(); + try { + Rectangle r = c.modelToView(dotOffset); + rsDotRect.y = r.y; + rsDotRect.height = r.height; + } catch (BadLocationException ex) { + // Leave rsDotRect unchanged + } + } + + /** + * Extend rectangular selection either by char in a specified selection + * or by word (if ctrl is pressed). + * + * @param toRight true for right or false for left. + * @param ctrl true for ctrl pressed. + */ + void extendRectangularSelection(boolean toRight, boolean ctrl) { + JTextComponent c = component; + Document doc = c.getDocument(); + int dotOffset = getDot(); + Element lineRoot = doc.getDefaultRootElement(); + int lineIndex = lineRoot.getElementIndex(dotOffset); + Element lineElement = lineRoot.getElement(lineIndex); + float charWidth; + LockedViewHierarchy lvh = ViewHierarchy.get(c).lock(); + try { + charWidth = lvh.getDefaultCharWidth(); + } finally { + lvh.unlock(); + } + int newDotOffset = -1; + try { + int newlineOffset = lineElement.getEndOffset() - 1; + Rectangle newlineRect = c.modelToView(newlineOffset); + if (!ctrl) { + if (toRight) { + if (rsDotRect.x < newlineRect.x) { + newDotOffset = dotOffset + 1; + } else { + rsDotRect.x += charWidth; + } + } else { // toLeft + if (rsDotRect.x > newlineRect.x) { + rsDotRect.x -= charWidth; + if (rsDotRect.x < newlineRect.x) { // Fix on rsDotRect + newDotOffset = newlineOffset; + } + } else { + newDotOffset = Math.max(dotOffset - 1, lineElement.getStartOffset()); + } + } + + } else { // With Ctrl + int numVirtualChars = 8; // Number of virtual characters per one Ctrl+Shift+Arrow press + if (toRight) { + if (rsDotRect.x < newlineRect.x) { +//[TODO] fix newDotOffset = Math.min(Utilities.getNextWord(c, dotOffset), lineElement.getEndOffset() - 1); + } else { // Extend virtually + rsDotRect.x += numVirtualChars * charWidth; + } + } else { // toLeft + if (rsDotRect.x > newlineRect.x) { // Virtually extended + rsDotRect.x -= numVirtualChars * charWidth; + if (rsDotRect.x < newlineRect.x) { + newDotOffset = newlineOffset; + } + } else { +//[TODO] fix newDotOffset = Math.max(Utilities.getPreviousWord(c, dotOffset), lineElement.getStartOffset()); + } + } + } + + if (newDotOffset != -1) { + rsDotRect = c.modelToView(newDotOffset); + moveDot(newDotOffset); // updates rs and fires state change + } else { + updateRectangularSelectionPaintRect(); + fireStateChanged(); + } + } catch (BadLocationException ex) { + // Leave selection as is + } + } + + + // Private implementation + + /** This method should only be accessed by transaction's methods */ + GapList getCaretItems() { + return caretItems; // No sync as this should only be accessed by transaction's methods + } + + /** This method should only be accessed by transaction's methods */ + GapList getSortedCaretItems() { + return sortedCaretItems; // No sync as this should only be accessed by transaction's methods + } + + /** This method may be accessed arbitrarily */ + private CaretItem getLastCaretItem() { + synchronized (listenerList) { + return caretItems.get(caretItems.size() - 1); + } + } + + /** + * Run a transaction to modify number of carets or their dots or selections. + * @param removeType type of carets removal. + * @param offset offset of document text removal otherwise the value is ignored. + * Internally this is also used for an end offset of the insertion at offset zero once it happens. + * @param addCarets carets to be added if any. + * @param moveHandler caret move handler or null if there's no caret moving. API client's move handlers + * are only invoked without any extra removals or additions so the original caret infos are used. + * Internal transactions may use caret additions or removals together with caret move handlers + * but they are also passed with original caret infos so the handlers must be aware of the removal + * and only pick the valid non-removed items. + * @return + */ + private int runTransaction(CaretTransaction.RemoveType removeType, int offset, CaretItem[] addCarets, CaretMoveHandler moveHandler) { + lock(); + try { + if (activeTransaction == null) { + JTextComponent c = component; + Document d = activeDoc; + if (c != null && d != null) { + activeTransaction = new CaretTransaction(this, c, d); + try { + activeTransaction.replaceCarets(removeType, offset, addCarets); + if (moveHandler != null) { + activeTransaction.runCaretMoveHandler(moveHandler); + } + activeTransaction.removeOverlappingRegions(); + int diffCount = 0; + synchronized (listenerList) { + GapList replaceItems = activeTransaction.getReplaceItems(); + if (replaceItems != null) { + diffCount = replaceItems.size() - caretItems.size(); + caretItems = replaceItems; + sortedCaretItems = activeTransaction.getSortedCaretItems(); + assert (sortedCaretItems != null) : "Null sortedCaretItems! removeType=" + removeType; // NOI18N + } + } + if (activeTransaction.isAnyChange()) { + caretInfos = null; + sortedCaretInfos = null; + } + pendingRepaintRemovedItemsList = activeTransaction. + addRemovedItems(pendingRepaintRemovedItemsList); + pendingUpdateVisualBoundsItemsList = activeTransaction. + addUpdateVisualBoundsItems(pendingUpdateVisualBoundsItemsList); + if (pendingUpdateVisualBoundsItemsList != null || pendingRepaintRemovedItemsList != null) { + // For now clear the lists and use old way TODO update to selective updating and rendering + fireStateChanged(); + dispatchUpdate(true); + pendingRepaintRemovedItemsList = null; + pendingUpdateVisualBoundsItemsList = null; + } + return diffCount; + } finally { + activeTransaction = null; + } + } + return Integer.MIN_VALUE; + + } else { // Nested transaction - document insert/remove within transaction + switch (removeType) { + case DOCUMENT_REMOVE: + activeTransaction.documentRemove(offset); + break; + case DOCUMENT_INSERT_ZERO_OFFSET: + activeTransaction.documentInsertAtZeroOffset(offset); + break; + default: + throw new AssertionError("Unsupported removeType=" + removeType + " in nested transaction"); // NOI18N + } + return 0; + } + } finally { + unlock(); + } + } + + private void moveDotCaret(int offset, CaretItem caret) throws IllegalStateException { + JTextComponent c = component; + AbstractDocument doc; + if (c != null && (doc = activeDoc) != null) { + if (offset >= 0 && offset <= doc.getLength()) { + doc.readLock(); + try { + int oldCaretPos = caret.getDot(); + if (offset == oldCaretPos) { // no change + return; + } + caret.setDotPos(doc.createPosition(offset)); + // Selection highlighting should be handled automatically by highlighting layers + if (rectangularSelection) { + Rectangle r = c.modelToView(offset); + if (rsDotRect != null) { + rsDotRect.y = r.y; + rsDotRect.height = r.height; + } else { + rsDotRect = r; + } + updateRectangularSelectionPaintRect(); + } + } catch (BadLocationException e) { + throw new IllegalStateException(e.toString()); + // position is incorrect + } finally { + doc.readUnlock(); + } + } + fireStateChanged(); + dispatchUpdate(true); + } + } + + private void fireEditorCaretChange(EditorCaretEvent evt) { + for (EditorCaretListener listener : listenerList.getListeners()) { + listener.caretChanged(evt); + } + } + + /** + * Notifies listeners that caret position has changed. + */ + private void fireStateChanged() { + Runnable runnable = new Runnable() { + public @Override void run() { + JTextComponent c = component; + if (c == null || c.getCaret() != EditorCaret.this) { + return; + } + fireEditorCaretChange(new EditorCaretEvent(EditorCaret.this, 0, Integer.MAX_VALUE)); // [TODO] temp firing without detailed info + ChangeEvent evt = new ChangeEvent(EditorCaret.this); + List listeners = changeListenerList.getListeners(); + for (ChangeListener l : listeners) { + l.stateChanged(evt); + } + } + }; + + // Always fire in EDT + if (inAtomicUnlock) { // Cannot fire within atomic lock9 + SwingUtilities.invokeLater(runnable); + } else { + ViewUtils.runInEDT(runnable); + } + updateSystemSelection(); + } + + private void lock() { + Thread curThread = Thread.currentThread(); + try { + synchronized (listenerList) { + while (lockThread != null) { + if (curThread == lockThread) { + lockDepth++; + return; + } + listenerList.wait(); + } + lockThread = curThread; + lockDepth = 1; + + } + } catch (InterruptedException e) { + throw new Error("Interrupted attempt to acquire lock"); // NOI18N + } + } + + private void unlock() { + Thread curThread = Thread.currentThread(); + synchronized (listenerList) { + if (lockThread == curThread) { + lockDepth--; + if (lockDepth == 0) { + lockThread = null; + listenerList.notifyAll(); + } + } else { + throw new IllegalStateException("Invalid thread called EditorCaret.unlock(): thread=" + // NOI18N + curThread + ", lockThread=" + lockThread); // NOI18N + } + } + } + + private void updateType() { + final JTextComponent c = component; + if (c != null) { + Color caretColor = null; + CaretType newType; + boolean cIsTextField = Boolean.TRUE.equals(c.getClientProperty("AsTextField")); + + if (cIsTextField) { + newType = CaretType.THIN_LINE_CARET; + } else if (prefs != null) { + String newTypeStr; + if (overwriteMode) { + newTypeStr = prefs.get(SimpleValueNames.CARET_TYPE_OVERWRITE_MODE, EditorPreferencesDefaults.defaultCaretTypeOverwriteMode); + } else { // insert mode + newTypeStr = prefs.get(SimpleValueNames.CARET_TYPE_INSERT_MODE, EditorPreferencesDefaults.defaultCaretTypeInsertMode); + this.thickCaretWidth = prefs.getInt(SimpleValueNames.THICK_CARET_WIDTH, EditorPreferencesDefaults.defaultThickCaretWidth); + } + newType = CaretType.decode(newTypeStr); + } else { + newType = CaretType.THICK_LINE_CARET; + } + + String mimeType = DocumentUtilities.getMimeType(c); + FontColorSettings fcs = (mimeType != null) ? MimeLookup.getLookup(mimeType).lookup(FontColorSettings.class) : null; + if (fcs != null) { + AttributeSet attribs = fcs.getFontColors(overwriteMode + ? FontColorNames.CARET_COLOR_OVERWRITE_MODE + : FontColorNames.CARET_COLOR_INSERT_MODE); //NOI18N + if (attribs != null) { + caretColor = (Color) attribs.getAttribute(StyleConstants.Foreground); + } + } + + this.type = newType; + final Color caretColorFinal = caretColor; + ViewUtils.runInEDT( + new Runnable() { + public @Override void run() { + if (caretColorFinal != null) { + c.setCaretColor(caretColorFinal); + if (LOG.isLoggable(Level.FINER)) { + LOG.finer("Updating caret color:" + caretColorFinal + '\n'); // NOI18N + } + } + + resetBlink(); + dispatchUpdate(false); + } + } + ); + } + } + + /** + * Assign new caret bounds into caretBounds variable. + * + * @return true if the new caret bounds were successfully computed + * and assigned or false otherwise. + */ + private boolean updateAllCaretsBounds() { + JTextComponent c = component; + AbstractDocument doc; + boolean ret = false; + if (c != null && (doc = activeDoc) != null) { + doc.readLock(); + try { + List sortedCarets = getSortedCarets(); + for (CaretInfo caret : sortedCarets) { + ret |= updateRealCaretBounds(caret.getCaretItem(), doc, c); + } + } finally { + doc.readUnlock(); + } + } + return ret; + } + + private boolean updateCaretBounds(CaretItem caret) { + JTextComponent c = component; + boolean ret = false; + AbstractDocument doc; + if (c != null && (doc = activeDoc) != null) { + doc.readLock(); + try { + ret = updateRealCaretBounds(caret, doc, c); + } finally { + doc.readUnlock(); + } + } + return ret; + } + + private boolean updateRealCaretBounds(CaretItem caret, Document doc, JTextComponent c) { + Position dotPos = caret.getDotPosition(); + int offset = dotPos == null? 0 : dotPos.getOffset(); + if (offset > doc.getLength()) { + offset = doc.getLength(); + } + Rectangle newCaretBounds; + try { + DocumentView docView = DocumentView.get(c); + if (docView != null) { + // docView.syncViewsRebuild(); // Make sure pending views changes are resolved + } + newCaretBounds = c.getUI().modelToView( + c, offset, Position.Bias.Forward); + // [TODO] Temporary fix - impl should remember real bounds computed by paintCustomCaret() + if (newCaretBounds != null) { + newCaretBounds.width = Math.max(newCaretBounds.width, 2); + } + + } catch (BadLocationException e) { + + newCaretBounds = null; + } + if (newCaretBounds != null) { + if (LOG.isLoggable(Level.FINE)) { + LOG.log(Level.FINE, "updateCaretBounds: old={0}, new={1}, offset={2}", + new Object[]{caret.getCaretBounds(), newCaretBounds, offset}); //NOI18N + } + caret.setCaretBounds(newCaretBounds); + return true; + } else { + return false; + } + } + + private void modelChanged(Document oldDoc, Document newDoc) { + if (oldDoc != null) { + // ideally the oldDoc param shouldn't exist and only listenDoc should be used + assert (oldDoc == activeDoc); + + DocumentUtilities.removeDocumentListener( + oldDoc, listenerImpl, DocumentListenerPriority.CARET_UPDATE); + AtomicLockDocument oldAtomicDoc = LineDocumentUtils.as(oldDoc, AtomicLockDocument.class); + if (oldAtomicDoc != null) { + oldAtomicDoc.removeAtomicLockListener(listenerImpl); + } + + activeDoc = null; + if (prefs != null && weakPrefsListener != null) { + prefs.removePreferenceChangeListener(weakPrefsListener); + } + } + + // EditorCaret only installs successfully into AbstractDocument based documents that carry a mime-type + if (newDoc instanceof AbstractDocument) { + String mimeType = DocumentUtilities.getMimeType(newDoc); + activeDoc = (AbstractDocument) newDoc; + DocumentUtilities.addDocumentListener( + newDoc, listenerImpl, DocumentListenerPriority.CARET_UPDATE); + AtomicLockDocument newAtomicDoc = LineDocumentUtils.as(oldDoc, AtomicLockDocument.class); + if (newAtomicDoc != null) { + newAtomicDoc.addAtomicLockListener(listenerImpl); + } + + // Set caret to zero position upon document change (DefaultCaret impl does this too) + runTransaction(CaretTransaction.RemoveType.REMOVE_ALL_CARETS, 0, + new CaretItem[] { new CaretItem(this, newDoc.getStartPosition(), null) }, null); + + // Leave caretPos and markPos null => offset==0 + prefs = (mimeType != null) ? MimeLookup.getLookup(mimeType).lookup(Preferences.class) : null; + if (prefs != null) { + weakPrefsListener = WeakListeners.create(PreferenceChangeListener.class, listenerImpl, prefs); + prefs.addPreferenceChangeListener(weakPrefsListener); + } + + updateType(); + } + } + + private void paintCaret(Graphics g, CaretItem caret) { + JTextComponent c = component; + if (c != null) { + g.setColor(c.getCaretColor()); + Rectangle caretBounds = caret.getCaretBounds(); + switch (type) { + case THICK_LINE_CARET: + g.fillRect(caretBounds.x, caretBounds.y, this.thickCaretWidth, caretBounds.height - 1); + break; + + case THIN_LINE_CARET: + int upperX = caret.getCaretBounds().x; + g.drawLine((int) upperX, caret.getCaretBounds().y, caret.getCaretBounds().x, + (caret.getCaretBounds().y + caret.getCaretBounds().height - 1)); + break; + + case BLOCK_CARET: + // Use a CaretOverwriteModeHighlighting layer to paint the caret + break; + + default: + throw new IllegalStateException("Invalid caret type=" + type); + } + } + } + + void dispatchUpdate() { + JTextComponent c = component; + if (c != null) { + dispatchUpdate(c.hasFocus()); // Scroll to caret only for component with focus + } + } + /** Update visual position of caret(s) */ + private void dispatchUpdate(final boolean scrollViewToCaret) { + /* Ensure that the caret's document listener will be added AFTER the views hierarchy's + * document listener so the code can run synchronously again + * which should eliminate the problem with caret lag. + * However the document can be modified from non-AWT thread + * which is the case in #57316 and in that case the code + * must run asynchronously in AWT thread. + */ + ViewUtils.runInEDT( + new Runnable() { + public @Override void run() { + AbstractDocument doc = activeDoc; + if (doc != null) { + doc.readLock(); + try { + update(scrollViewToCaret); + } finally { + doc.readUnlock(); + } + } + } + } + ); + } + + /** + * Update the caret's visual position. + *
    + * The document is read-locked while calling this method. + * + * @param scrollViewToCaret whether the view of the text component should be + * scrolled to the position of the caret. + */ + private void update(boolean scrollViewToCaret) { + caretUpdatePending = false; + JTextComponent c = component; + if (c != null) { + if (!c.isValid()) { + c.validate(); + } + Document doc = c.getDocument(); + if (doc != null) { + List sortedCarets = getSortedCarets(); + for (CaretInfo caret : sortedCarets) { + CaretItem caretItem = caret.getCaretItem(); + Rectangle oldCaretBounds = caretItem.getCaretBounds(); // no need to deep copy + if (oldCaretBounds != null) { + c.repaint(oldCaretBounds); + } + + // note - the order is important ! caret bounds must be updated even if the fold flag is true. + if (updateCaretBounds(caretItem) || updateAfterFoldHierarchyChange) { + Rectangle scrollBounds = new Rectangle(caretItem.getCaretBounds()); + + // Optimization to avoid extra repaint: + // If the caret bounds were not yet assigned then attempt + // to scroll the window so that there is an extra vertical space + // for the possible horizontal scrollbar that may appear + // if the line-view creation process finds line-view that + // is too wide and so the horizontal scrollbar will appear + // consuming an extra vertical space at the bottom. + if (oldCaretBounds == null) { + Component viewport = c.getParent(); + if (viewport instanceof JViewport) { + Component scrollPane = viewport.getParent(); + if (scrollPane instanceof JScrollPane) { + JScrollBar hScrollBar = ((JScrollPane) scrollPane).getHorizontalScrollBar(); + if (hScrollBar != null) { + int hScrollBarHeight = hScrollBar.getPreferredSize().height; + Dimension extentSize = ((JViewport) viewport).getExtentSize(); + // If the extent size is high enough then extend + // the scroll region by extra vertical space + if (extentSize.height >= caretItem.getCaretBounds().height + hScrollBarHeight) { + scrollBounds.height += hScrollBarHeight; + } + } + } + } + } + + Rectangle visibleBounds = c.getVisibleRect(); + + // If folds have changed attempt to scroll the view so that + // relative caret's visual position gets retained + // (the absolute position will change because of collapsed/expanded folds). + boolean doScroll = scrollViewToCaret; + boolean explicit = false; + if (oldCaretBounds != null && (!scrollViewToCaret || updateAfterFoldHierarchyChange)) { + int oldRelY = oldCaretBounds.y - visibleBounds.y; + // Only fix if the caret is within visible bounds and the new x or y coord differs from the old one + if (LOG.isLoggable(Level.FINER)) { + LOG.log(Level.FINER, "oldCaretBounds: {0}, visibleBounds: {1}, caretBounds: {2}", + new Object[]{oldCaretBounds, visibleBounds, caretItem.getCaretBounds()}); + } + if (oldRelY >= 0 && oldRelY < visibleBounds.height + && (oldCaretBounds.y != caretItem.getCaretBounds().y || oldCaretBounds.x != caretItem.getCaretBounds().x)) { + doScroll = true; // Perform explicit scrolling + explicit = true; + int oldRelX = oldCaretBounds.x - visibleBounds.x; + // Do not retain the horizontal caret bounds by scrolling + // since many modifications do not explicitly say that they are typing modifications + // and this would cause problems like #176268 +// scrollBounds.x = Math.max(caretBounds.x - oldRelX, 0); + scrollBounds.y = Math.max(caretItem.getCaretBounds().y - oldRelY, 0); +// scrollBounds.width = visibleBounds.width; + scrollBounds.height = visibleBounds.height; + } + } + + // Historically the caret is expected to appear + // in the middle of the window if setDot() gets called + // e.g. by double-clicking in Navigator. + // If the caret bounds are more than a caret height below the present + // visible view bounds (or above the view bounds) + // then scroll the window so that the caret is in the middle + // of the visible window to see the context around the caret. + // This should work fine with PgUp/Down because these + // scroll the view explicitly. + if (scrollViewToCaret + && !explicit + && // #219580: if the preceding if-block computed new scrollBounds, it cannot be offset yet more + /* # 70915 !updateAfterFoldHierarchyChange && */ (caretItem.getCaretBounds().y > visibleBounds.y + visibleBounds.height + caretItem.getCaretBounds().height + || caretItem.getCaretBounds().y + caretItem.getCaretBounds().height < visibleBounds.y - caretItem.getCaretBounds().height)) { + // Scroll into the middle + scrollBounds.y -= (visibleBounds.height - caretItem.getCaretBounds().height) / 2; + scrollBounds.height = visibleBounds.height; + } + if (LOG.isLoggable(Level.FINER)) { + LOG.finer("Resetting fold flag, current: " + updateAfterFoldHierarchyChange); + } + updateAfterFoldHierarchyChange = false; + + // Ensure that the viewport will be scrolled either to make the caret visible + // or to retain cart's relative visual position against the begining of the viewport's visible rectangle. + if (doScroll) { + if (LOG.isLoggable(Level.FINER)) { + LOG.finer("Scrolling to: " + scrollBounds); + } + c.scrollRectToVisible(scrollBounds); + if (!c.getVisibleRect().intersects(scrollBounds)) { + // HACK: see #219580: for some reason, the scrollRectToVisible may fail. + c.scrollRectToVisible(scrollBounds); + } + } + resetBlink(); + c.repaint(caretItem.getCaretBounds()); + } + } + } + } + } + + private void updateSystemSelection() { + if(component == null) return; + Clipboard clip = null; + try { + clip = component.getToolkit().getSystemSelection(); + } catch (SecurityException ex) { + // XXX: ignore for now, there is no ExClipboard for SystemSelection Clipboard + } + if(clip != null) { + StringBuilder builder = new StringBuilder(); + boolean first = true; + List sortedCarets = getSortedCarets(); + for (CaretInfo caret : sortedCarets) { + CaretItem caretItem = caret.getCaretItem(); + if(caretItem.isSelection()) { + if(!first) { + builder.append("\n"); + } else { + first = false; + } + builder.append(getSelectedText(caretItem)); + } + } + if(builder.length() > 0) { + clip.setContents(new java.awt.datatransfer.StringSelection(builder.toString()), null); + } + } + } + + /** + * Returns the selected text contained for this + * Caret. If the selection is + * null or the document empty, returns null. + * + * @param caret + * @return the text + * @exception IllegalArgumentException if the selection doesn't + * have a valid mapping into the document for some reason + */ + private String getSelectedText(CaretItem caret) { + String txt = null; + int p0 = Math.min(caret.getDot(), caret.getMark()); + int p1 = Math.max(caret.getDot(), caret.getMark()); + if (p0 != p1) { + try { + Document doc = component.getDocument(); + txt = doc.getText(p0, p1 - p0); + } catch (BadLocationException e) { + throw new IllegalArgumentException(e.getMessage()); + } + } + return txt; + } + + private void updateRectangularSelectionPositionBlocks() { + JTextComponent c = component; + if (rectangularSelection) { + AbstractDocument doc = activeDoc; + if (doc != null) { + doc.readLock(); + try { + if (rsRegions == null) { + rsRegions = new ArrayList(); + component.putClientProperty(RECTANGULAR_SELECTION_REGIONS_PROPERTY, rsRegions); + } + synchronized (rsRegions) { + if (LOG.isLoggable(Level.FINE)) { + LOG.fine("Rectangular-selection position regions:\n"); + } + rsRegions.clear(); + if (rsPaintRect != null) { + LockedViewHierarchy lvh = ViewHierarchy.get(c).lock(); + try { + float rowHeight = lvh.getDefaultRowHeight(); + double y = rsPaintRect.y; + double maxY = y + rsPaintRect.height; + double minX = rsPaintRect.getMinX(); + double maxX = rsPaintRect.getMaxX(); + do { + int startOffset = lvh.viewToModel(minX, y, null); + int endOffset = lvh.viewToModel(maxX, y, null); + // They could be swapped due to RTL text + if (startOffset > endOffset) { + int tmp = startOffset; + startOffset = endOffset; + endOffset = tmp; + } + Position startPos = activeDoc.createPosition(startOffset); + Position endPos = activeDoc.createPosition(endOffset); + rsRegions.add(startPos); + rsRegions.add(endPos); + if (LOG.isLoggable(Level.FINE)) { + LOG.fine(" <" + startOffset + "," + endOffset + ">\n"); + } + y += rowHeight; + } while (y < maxY); + c.putClientProperty(RECTANGULAR_SELECTION_REGIONS_PROPERTY, rsRegions); + } finally { + lvh.unlock(); + } + } + } + } catch (BadLocationException ex) { + Exceptions.printStackTrace(ex); + } finally { + doc.readUnlock(); + } + } + } + } + + private String dumpVisibility() { + return "visible=" + isVisible() + ", blinkVisible=" + blinkVisible; + } + + /*private*/ void resetBlink() { + boolean visible = isVisible(); + synchronized (listenerList) { + if (flasher != null) { + flasher.stop(); + setBlinkVisible(true); + if (visible) { + if (LOG.isLoggable(Level.FINER)){ + LOG.finer("Reset blinking (caret already visible)" + // NOI18N + " - starting the caret blinking timer: " + dumpVisibility() + '\n'); // NOI18N + } + flasher.start(); + } else { + if (LOG.isLoggable(Level.FINER)){ + LOG.finer("Reset blinking (caret not visible)" + // NOI18N + " - caret blinking timer not started: " + dumpVisibility() + '\n'); // NOI18N + } + } + } + } + } + + /*private*/ void setBlinkVisible(boolean blinkVisible) { + synchronized (listenerList) { + this.blinkVisible = blinkVisible; + } + updateOverwriteModeLayer(false); + } + + private void updateOverwriteModeLayer(boolean forceUpdate) { + JTextComponent c; + if ((forceUpdate || overwriteMode) && (c = component) != null) { + CaretOverwriteModeHighlighting overwriteModeHighlighting = (CaretOverwriteModeHighlighting) + c.getClientProperty(CaretOverwriteModeHighlighting.class); + if (overwriteModeHighlighting != null) { + overwriteModeHighlighting.setVisible(visible && blinkVisible); + } + } + } + + private void adjustRectangularSelectionMouseX(int x, int y) { + if (!rectangularSelection) { + return; + } + JTextComponent c = component; + int offset = c.viewToModel(new Point(x, y)); + Rectangle r = null;; + if (offset >= 0) { + try { + r = c.modelToView(offset); + } catch (BadLocationException ex) { + r = null; + } + } + if (r != null) { + float xDiff = x - r.x; + if (xDiff > 0) { + float charWidth; + LockedViewHierarchy lvh = ViewHierarchy.get(c).lock(); + try { + charWidth = lvh.getDefaultCharWidth(); + } finally { + lvh.unlock(); + } + int n = (int) (xDiff / charWidth); + r.x += n * charWidth; + r.width = (int) charWidth; + } + rsDotRect.x = r.x; + rsDotRect.width = r.width; + updateRectangularSelectionPaintRect(); + fireStateChanged(); + } + } + + private void updateRectangularSelectionPaintRect() { + // Repaint current rect + JTextComponent c = component; + Rectangle repaintRect = rsPaintRect; + if (rsDotRect == null || rsMarkRect == null) { + return; + } + Rectangle newRect = new Rectangle(); + if (rsDotRect.x < rsMarkRect.x) { // Swap selection to left + newRect.x = rsDotRect.x; // -1 to make the visual selection non-empty + newRect.width = rsMarkRect.x - newRect.x; + } else { // Extend or shrink on right + newRect.x = rsMarkRect.x; + newRect.width = rsDotRect.x - newRect.x; + } + if (rsDotRect.y < rsMarkRect.y) { + newRect.y = rsDotRect.y; + newRect.height = (rsMarkRect.y + rsMarkRect.height) - newRect.y; + } else { + newRect.y = rsMarkRect.y; + newRect.height = (rsDotRect.y + rsDotRect.height) - newRect.y; + } + if (newRect.width < 2) { + newRect.width = 2; + } + rsPaintRect = newRect; + + // Repaint merged region with original rect + if (repaintRect == null) { + repaintRect = rsPaintRect; + } else { + repaintRect = repaintRect.union(rsPaintRect); + } + c.repaint(repaintRect); + + updateRectangularSelectionPositionBlocks(); + } + + private void selectEnsureMinSelection(int mark, int dot, int newDot) { + if (LOG.isLoggable(Level.FINE)) { + LOG.fine("selectEnsureMinSelection: mark=" + mark + ", dot=" + dot + ", newDot=" + newDot); // NOI18N + } + if (dot >= mark) { // Existing forward selection + if (newDot >= mark) { + moveDot(Math.max(newDot, minSelectionEndOffset)); + } else { // newDot < mark => swap mark and dot + setDot(minSelectionEndOffset); + moveDot(Math.min(newDot, minSelectionStartOffset)); + } + + } else { // Existing backward selection + if (newDot <= mark) { + moveDot(Math.min(newDot, minSelectionStartOffset)); + } else { // newDot > mark => swap mark and dot + setDot(minSelectionStartOffset); + moveDot(Math.max(newDot, minSelectionEndOffset)); + } + } + } + + private boolean isLeftMouseButtonExt(MouseEvent evt) { + return (SwingUtilities.isLeftMouseButton(evt) + && !(evt.isPopupTrigger()) + && (evt.getModifiers() & (InputEvent.META_MASK/* | InputEvent.ALT_MASK*/)) == 0); + } + + private boolean isMiddleMouseButtonExt(MouseEvent evt) { + return (evt.getButton() == MouseEvent.BUTTON2) && + (evt.getModifiersEx() & (InputEvent.CTRL_DOWN_MASK | InputEvent.META_DOWN_MASK | /* cannot be tested bcs of bug in JDK InputEvent.ALT_DOWN_MASK | */ InputEvent.ALT_GRAPH_DOWN_MASK)) == 0; + } + + private int mapDragOperationFromModifiers(MouseEvent e) { + int mods = e.getModifiersEx(); + + if ((mods & InputEvent.BUTTON1_DOWN_MASK) == 0) { + return TransferHandler.NONE; + } + + return TransferHandler.COPY_OR_MOVE; + } + + /** + * Determines if the following are true: + *

      + *
    • the press event is located over a selection + *
    • the dragEnabled property is true + *
    • A TranferHandler is installed + *
    + *

    + * This is implemented to check for a TransferHandler. + * Subclasses should perform the remaining conditions. + */ + private boolean isDragPossible(MouseEvent e) { + Object src = e.getSource(); + if (src instanceof JComponent) { + JComponent comp = (JComponent) src; + boolean possible = (comp == null) ? false : (comp.getTransferHandler() != null); + if (possible && comp instanceof JTextComponent) { + JTextComponent c = (JTextComponent) comp; + if (c.getDragEnabled()) { + Caret caret = c.getCaret(); + int dot = caret.getDot(); + int mark = caret.getMark(); + if (dot != mark) { + Point p = new Point(e.getX(), e.getY()); + int pos = c.viewToModel(p); + + int p0 = Math.min(dot, mark); + int p1 = Math.max(dot, mark); + if ((pos >= p0) && (pos < p1)) { + return true; + } + } + } + } + } + return false; + } + + private void refresh() { + updateType(); + SwingUtilities.invokeLater(new Runnable() { + public @Override void run() { + updateAllCaretsBounds(); // the line height etc. may have change + } + }); + } + + private static String logMouseEvent(MouseEvent evt) { + return "x=" + evt.getX() + ", y=" + evt.getY() + ", clicks=" + evt.getClickCount() //NOI18N + + ", component=" + s2s(evt.getComponent()) //NOI18N + + ", source=" + s2s(evt.getSource()) + ", button=" + evt.getButton() + ", mods=" + evt.getModifiers() + ", modsEx=" + evt.getModifiersEx(); //NOI18N + } + + private static String s2s(Object o) { + return o == null ? "null" : o.getClass().getName() + "@" + Integer.toHexString(System.identityHashCode(o)); //NOI18N + } + + private final class ListenerImpl extends ComponentAdapter + implements DocumentListener, AtomicLockListener, MouseListener, MouseMotionListener, FocusListener, ViewHierarchyListener, + PropertyChangeListener, ActionListener, PreferenceChangeListener, KeyListener + { + + ListenerImpl() { + } + + public @Override void preferenceChange(PreferenceChangeEvent evt) { + String setingName = evt == null ? null : evt.getKey(); + if (setingName == null || SimpleValueNames.CARET_BLINK_RATE.equals(setingName)) { + int rate = prefs.getInt(SimpleValueNames.CARET_BLINK_RATE, -1); + if (rate == -1) { + rate = EditorPreferencesDefaults.defaultCaretBlinkRate; + } + setBlinkRate(rate); + refresh(); + } + } + + public @Override void propertyChange(PropertyChangeEvent evt) { + String propName = evt.getPropertyName(); + JTextComponent c = component; + if ("document".equals(propName)) { // NOI18N + if (c != null) { + modelChanged(activeDoc, c.getDocument()); + } + + } else if (EditorUtilities.CARET_OVERWRITE_MODE_PROPERTY.equals(propName)) { + Boolean b = (Boolean) evt.getNewValue(); + overwriteMode = (b != null) ? b : false; + updateOverwriteModeLayer(true); + updateType(); + + } else if ("ancestor".equals(propName) && evt.getSource() == component) { // NOI18N + // The following code ensures that when the width of the line views + // gets computed on background after the file gets opened + // (so the horizontal scrollbar gets added after several seconds + // for larger files) that the suddenly added horizontal scrollbar + // will not hide the caret laying on the last line of the viewport. + // A component listener gets installed into horizontal scrollbar + // and if it's fired the caret's bounds will be checked whether + // they intersect with the horizontal scrollbar + // and if so the view will be scrolled. + Container parent = component.getParent(); + if (parent instanceof JViewport) { + parent = parent.getParent(); // parent of viewport + if (parent instanceof JScrollPane) { + JScrollPane scrollPane = (JScrollPane) parent; + JScrollBar hScrollBar = scrollPane.getHorizontalScrollBar(); + if (hScrollBar != null) { + // Add weak listener so that editor pane could be removed + // from scrollpane without being held by scrollbar + hScrollBar.addComponentListener( + (ComponentListener) WeakListeners.create( + ComponentListener.class, listenerImpl, hScrollBar)); + } + } + } + } else if ("enabled".equals(propName)) { + Boolean enabled = (Boolean) evt.getNewValue(); + if (component.isFocusOwner()) { + if (enabled == Boolean.TRUE) { + if (component.isEditable()) { + setVisible(true); + } + setSelectionVisible(true); + } else { + setVisible(false); + setSelectionVisible(false); + } + } + } else if (RECTANGULAR_SELECTION_PROPERTY.equals(propName)) { + boolean origRectangularSelection = rectangularSelection; + rectangularSelection = Boolean.TRUE.equals(component.getClientProperty(RECTANGULAR_SELECTION_PROPERTY)); + if (rectangularSelection != origRectangularSelection) { + if (rectangularSelection) { + setRectangularSelectionToDotAndMark(); + RectangularSelectionTransferHandler.install(component); + + } else { // No rectangular selection + RectangularSelectionTransferHandler.uninstall(component); + } + fireStateChanged(); + } + } + } + + // ActionListener methods + /** + * Fired when blink timer fires + */ + public @Override void actionPerformed(ActionEvent evt) { + JTextComponent c = component; + if (c != null) { + setBlinkVisible(!blinkVisible); + List sortedCarets = getSortedCarets(); // TODO only repaint carets showing on screen + for (CaretInfo caret : sortedCarets) { + CaretItem caretItem = caret.getCaretItem(); + if (caretItem.getCaretBounds() != null) { + Rectangle repaintRect = caretItem.getCaretBounds(); + c.repaint(repaintRect); + } + } + } + } + + // DocumentListener methods + public @Override void insertUpdate(DocumentEvent evt) { + JTextComponent c = component; + if (c != null) { + Document doc = evt.getDocument(); + int offset = evt.getOffset(); + final int endOffset = offset + evt.getLength(); + if (offset == 0) { + // Manually shift carets at offset zero + runTransaction(CaretTransaction.RemoveType.DOCUMENT_INSERT_ZERO_OFFSET, endOffset, null, null); + } + // [TODO] proper undo solution + modified = true; + modifiedUpdate(true); + + } + } + + public @Override void removeUpdate(DocumentEvent evt) { + JTextComponent c = component; + if (c != null) { + // [TODO] proper undo solution + modified = true; + int offset = evt.getOffset(); + runTransaction(CaretTransaction.RemoveType.DOCUMENT_REMOVE, offset, null, null); + modifiedUpdate(true); + } + } + + public @Override void changedUpdate(DocumentEvent evt) { + } + + public @Override + void atomicLock(AtomicLockEvent evt) { + inAtomicLock = true; + } + + public @Override + void atomicUnlock(AtomicLockEvent evt) { + inAtomicLock = false; + inAtomicUnlock = true; + try { + modifiedUpdate(typingModificationOccurred); + } finally { + inAtomicUnlock = false; + typingModificationOccurred = false; + } + } + + private void modifiedUpdate(boolean typingModification) { + if (!inAtomicLock) { + JTextComponent c = component; + if (modified && c != null) { + fireStateChanged(); + modified = false; + } + } else { + typingModificationOccurred |= typingModification; + } + } + + // MouseListener methods + @Override + public void mousePressed(MouseEvent evt) { + if (LOG.isLoggable(Level.FINE)) { + LOG.fine("mousePressed: " + logMouseEvent(evt) + ", state=" + mouseState + '\n'); // NOI18N + } + + JTextComponent c = component; + AbstractDocument doc = activeDoc; + if (c != null && doc != null && isLeftMouseButtonExt(evt)) { + doc.readLock(); + try { + // Expand fold if offset is in collapsed fold + int offset = mouse2Offset(evt); + switch (evt.getClickCount()) { + case 1: // Single press + if (c.isEnabled() && !c.hasFocus()) { + c.requestFocus(); + } + c.setDragEnabled(true); + if (evt.isAltDown() && evt.isShiftDown()) { + mouseState = MouseState.CHAR_SELECTION; + try { + Position pos = doc.createPosition(offset); + runTransaction(CaretTransaction.RemoveType.NO_REMOVE, 0, + new CaretItem[] { new CaretItem(EditorCaret.this, pos, pos) }, null); + } catch (BadLocationException ex) { + // Do nothing + } + } else if (evt.isShiftDown()) { // Select till offset + moveDot(offset); + adjustRectangularSelectionMouseX(evt.getX(), evt.getY()); // also fires state change + mouseState = MouseState.CHAR_SELECTION; + } else // Regular press + // check whether selection drag is possible + if (isDragPossible(evt) && mapDragOperationFromModifiers(evt) != TransferHandler.NONE) { + mouseState = MouseState.DRAG_SELECTION_POSSIBLE; + } else { // Drag not possible + mouseState = MouseState.CHAR_SELECTION; + setDot(offset); + } + break; + + case 2: // double-click => word selection + mouseState = MouseState.WORD_SELECTION; + // Disable drag which would otherwise occur when mouse would be over text + c.setDragEnabled(false); + // Check possible fold expansion + 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. + @SuppressWarnings("unchecked") + Callable cc = (Callable) c.getClientProperty("org.netbeans.api.fold.expander"); + if (cc == null || !cc.equals(this)) { + if (selectWordAction == null) { + selectWordAction = EditorActionUtilities.getAction( + c.getUI().getEditorKit(c), DefaultEditorKit.selectWordAction); + } + 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; + + case 3: // triple-click => line selection + mouseState = MouseState.LINE_SELECTION; + // Disable drag which would otherwise occur when mouse would be over text + c.setDragEnabled(false); + if (selectLineAction == null) { + selectLineAction = EditorActionUtilities.getAction( + c.getUI().getEditorKit(c), DefaultEditorKit.selectLineAction); + } + if (selectLineAction != null) { + selectLineAction.actionPerformed(null); + // Select word action selects forward i.e. dot > mark + minSelectionStartOffset = getMark(); + minSelectionEndOffset = getDot(); + } + break; + + default: // multi-click + } + } finally { + doc.readUnlock(); + } + } + } + + @Override + public void mouseReleased(MouseEvent evt) { + if (LOG.isLoggable(Level.FINE)) { + LOG.fine("mouseReleased: " + logMouseEvent(evt) + ", state=" + mouseState + '\n'); // NOI18N + } + + int offset = mouse2Offset(evt); + switch (mouseState) { + case DRAG_SELECTION_POSSIBLE: + setDot(offset); + adjustRectangularSelectionMouseX(evt.getX(), evt.getY()); // also fires state change + break; + + case CHAR_SELECTION: + if (evt.isAltDown() && evt.isShiftDown()) { + moveDotCaret(offset, getLastCaretItem()); + } else { + moveDot(offset); // Will do setDot() if no selection + adjustRectangularSelectionMouseX(evt.getX(), evt.getY()); // also fires state change + } + break; + } + // Set DEFAULT state; after next mouse press the state may change + // to another state according to particular click count + mouseState = MouseState.DEFAULT; + component.setDragEnabled(true); + } + + /** + * Translates mouse event to text offset + */ + int mouse2Offset(MouseEvent evt) { + JTextComponent c = component; + int offset = 0; + if (c != null) { + int y = evt.getY(); + if (y < 0) { + offset = 0; + } else if (y > c.getSize().getHeight()) { + offset = c.getDocument().getLength(); + } else { + offset = c.viewToModel(new Point(evt.getX(), evt.getY())); + } + } + return offset; + } + + @Override + public void mouseClicked(MouseEvent evt) { + if (LOG.isLoggable(Level.FINE)) { + LOG.fine("mouseClicked: " + logMouseEvent(evt) + ", state=" + mouseState + '\n'); // NOI18N + } + + JTextComponent c = component; + if (c != null) { + if (isMiddleMouseButtonExt(evt)) { + if (evt.getClickCount() == 1) { + if (c == null) { + return; + } + Clipboard buffer = component.getToolkit().getSystemSelection(); + + if (buffer == null) { + return; + } + + Transferable trans = buffer.getContents(null); + if (trans == null) { + return; + } + + final Document doc = c.getDocument(); + if (doc == null) { + return; + } + + final int offset = c.getUI().viewToModel(c, new Point(evt.getX(), evt.getY())); + + try { + final String pastingString = (String) trans.getTransferData(DataFlavor.stringFlavor); + if (pastingString == null) { + return; + } + Runnable pasteRunnable = new Runnable() { + public @Override + void run() { + try { + doc.insertString(offset, pastingString, null); + setDot(offset + pastingString.length()); + } catch (BadLocationException exc) { + } + } + }; + AtomicLockDocument ald = LineDocumentUtils.as(doc, AtomicLockDocument.class); + if (ald != null) { + ald.runAtomic(pasteRunnable); + } else { + pasteRunnable.run(); + } + } catch (UnsupportedFlavorException ufe) { + } catch (IOException ioe) { + } + } + } + } + } + + @Override + public void mouseEntered(MouseEvent evt) { + } + + @Override + public void mouseExited(MouseEvent evt) { + } + + // MouseMotionListener methods + @Override + public void mouseMoved(MouseEvent evt) { + if (mouseState == MouseState.DEFAULT) { + boolean textCursor = true; + int position = component.viewToModel(evt.getPoint()); + if (RectangularSelectionUtils.isRectangularSelection(component)) { + List positions = RectangularSelectionUtils.regionsCopy(component); + for (int i = 0; textCursor && i < positions.size(); i += 2) { + int a = positions.get(i).getOffset(); + int b = positions.get(i + 1).getOffset(); + if (a == b) { + continue; + } + + textCursor &= !(position >= a && position <= b || position >= b && position <= a); + } + } else // stream selection + if (getDot() == getMark()) { + // empty selection + textCursor = true; + } else { + int dot = getDot(); + int mark = getMark(); + if (position >= dot && position <= mark || position >= mark && position <= dot) { + textCursor = false; + } else { + textCursor = true; + } + } + + if (textCursor != showingTextCursor) { + int cursorType = textCursor ? Cursor.TEXT_CURSOR : Cursor.DEFAULT_CURSOR; + component.setCursor(Cursor.getPredefinedCursor(cursorType)); + showingTextCursor = textCursor; + } + } + } + + @Override + public void mouseDragged(MouseEvent evt) { + if (LOG.isLoggable(Level.FINE)) { + LOG.fine("mouseDragged: " + logMouseEvent(evt) + ", state=" + mouseState + '\n'); //NOI18N + } + + if (isLeftMouseButtonExt(evt)) { + JTextComponent c = component; + int offset = mouse2Offset(evt); + int dot = getDot(); + int mark = getMark(); + LineDocument lineDoc = LineDocumentUtils.asRequired(c.getDocument(), LineDocument.class); + + try { + switch (mouseState) { + case DEFAULT: + case DRAG_SELECTION: + break; + + case DRAG_SELECTION_POSSIBLE: + mouseState = MouseState.DRAG_SELECTION; + break; + + case CHAR_SELECTION: + if (evt.isAltDown() && evt.isShiftDown()) { + moveDotCaret(offset, getLastCaretItem()); + } else { + moveDot(offset); + adjustRectangularSelectionMouseX(evt.getX(), evt.getY()); + } + break; // Use the offset under mouse pointer + + case WORD_SELECTION: + // Increase selection if at least in the middle of a word. + // It depends whether selection direction is from lower offsets upward or back. + if (offset >= mark) { // Selection extends forward. + offset = LineDocumentUtils.getWordEnd(lineDoc, offset); + } else { // Selection extends backward. + offset = LineDocumentUtils.getWordStart(lineDoc, offset); + } + selectEnsureMinSelection(mark, dot, offset); + break; + + case LINE_SELECTION: + if (offset >= mark) { // Selection extends forward + offset = Math.min(LineDocumentUtils.getLineEnd(lineDoc, offset) + 1, c.getDocument().getLength()); + } else { // Selection extends backward + offset = LineDocumentUtils.getLineStart(lineDoc, offset); + } + selectEnsureMinSelection(mark, dot, offset); + break; + + default: + throw new AssertionError("Invalid state " + mouseState); // NOI18N + } + } catch (BadLocationException ex) { + Exceptions.printStackTrace(ex); + } + } + } + + // FocusListener methods + public @Override void focusGained(FocusEvent evt) { + if (LOG.isLoggable(Level.FINE)) { + LOG.fine( + "BaseCaret.focusGained(); doc=" + // NOI18N + component.getDocument().getProperty(Document.TitleProperty) + '\n' + ); + } + + JTextComponent c = component; + if (c != null) { + updateType(); + if (component.isEnabled()) { + if (component.isEditable()) { + setVisible(true); + } + setSelectionVisible(true); + } + if (LOG.isLoggable(Level.FINER)) { + LOG.finer("Caret visibility: " + isVisible() + '\n'); // NOI18N + } + } else { + if (LOG.isLoggable(Level.FINER)) { + LOG.finer("Text component is null, caret will not be visible" + '\n'); // NOI18N + } + } + } + + public @Override void focusLost(FocusEvent evt) { + if (LOG.isLoggable(Level.FINE)) { + LOG.fine("BaseCaret.focusLost(); doc=" + // NOI18N + component.getDocument().getProperty(Document.TitleProperty) + + "\nFOCUS GAINER: " + evt.getOppositeComponent() + '\n' // NOI18N + ); + if (LOG.isLoggable(Level.FINER)) { + LOG.finer("FOCUS EVENT: " + evt + '\n'); // NOI18N + } + } + setVisible(false); + setSelectionVisible(evt.isTemporary()); + } + + // ComponentListener methods + /** + * May be called for either component or horizontal scrollbar. + */ + public @Override void componentShown(ComponentEvent e) { + // Called when horizontal scrollbar gets visible + // (but the same listener added to component as well so must check first) + // Check whether present caret position will not get hidden + // under horizontal scrollbar and if so scroll the view + Component hScrollBar = e.getComponent(); + if (hScrollBar != component) { // really called for horizontal scrollbar + Component scrollPane = hScrollBar.getParent(); + boolean needsUpdate = false; + List sortedCarets = getSortedCarets(); + for (CaretInfo caret : sortedCarets) { // TODO This is wrong, but a quick prototype + CaretItem caretItem = caret.getCaretItem(); + if (caretItem.getCaretBounds() != null && scrollPane instanceof JScrollPane) { + Rectangle viewRect = ((JScrollPane)scrollPane).getViewport().getViewRect(); + Rectangle hScrollBarRect = new Rectangle( + viewRect.x, + viewRect.y + viewRect.height, + hScrollBar.getWidth(), + hScrollBar.getHeight() + ); + if (hScrollBarRect.intersects(caretItem.getCaretBounds())) { + // Update caret's position + needsUpdate = true; + } + } + } + if(needsUpdate) { + dispatchUpdate(true); // should be visible so scroll the view + } + } + } + + /** + * May be called for either component or horizontal scrollbar. + */ + public @Override void componentResized(ComponentEvent e) { + Component c = e.getComponent(); + if (c == component) { // called for component + // In case the caretBounds are still null + // (component not connected to hierarchy yet or it has zero size + // so the modelToView() returned null) re-attempt to compute the bounds. + CaretItem caret = getLastCaretItem(); + if (caret.getCaretBounds() == null) { + dispatchUpdate(true); + if (caret.getCaretBounds() != null) { // detach the listener - no longer necessary + c.removeComponentListener(this); + } + } + } + } + + @Override + public void viewHierarchyChanged(ViewHierarchyEvent evt) { + if (!caretUpdatePending) { + caretUpdatePending = true; + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + update(false); + } + }); + } + } + + @Override + public void keyPressed(KeyEvent e) { + if (e.getKeyCode() == KeyEvent.VK_ESCAPE) { + // Retain just the last caret + retainLastCaretOnly(); + } + } + + @Override + public void keyTyped(KeyEvent e) { + } + + @Override + public void keyReleased(KeyEvent e) { + } + + } // End of ListenerImpl class + + + private enum CaretType { + + /** + * Two-pixel line caret (the default). + */ + THICK_LINE_CARET, + + /** + * Thin one-pixel line caret. + */ + THIN_LINE_CARET, + + /** + * Rectangle corresponding to a single character. + */ + BLOCK_CARET; + + static CaretType decode(String typeStr) { + switch (typeStr) { + case EditorPreferencesDefaults.THIN_LINE_CARET: + return THIN_LINE_CARET; + case EditorPreferencesDefaults.BLOCK_CARET: + return BLOCK_CARET; + default: + return THICK_LINE_CARET; + } + }; + + } + + private static enum MouseState { + + DEFAULT, // Mouse released; not extending any selection + CHAR_SELECTION, // Extending character selection after single mouse press + WORD_SELECTION, // Extending word selection after double-click when mouse button still pressed + LINE_SELECTION, // Extending line selection after triple-click when mouse button still pressed + DRAG_SELECTION_POSSIBLE, // There was a selected text when mouse press arrived so drag is possible + DRAG_SELECTION // Drag is being done (text selection existed at the mouse press) + + } + +} diff --git a/editor.lib2/src/org/netbeans/api/editor/caret/EditorCaretEvent.java b/editor.lib2/src/org/netbeans/api/editor/caret/EditorCaretEvent.java new file mode 100644 --- /dev/null +++ b/editor.lib2/src/org/netbeans/api/editor/caret/EditorCaretEvent.java @@ -0,0 +1,94 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2015 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 2015 Sun Microsystems, Inc. + */ +package org.netbeans.api.editor.caret; + +import org.netbeans.api.annotations.common.NonNull; + +/** + * Notification event about changes in editor caret. + * + * @author Miloslav Metelka + * @since 2.6 + */ +public final class EditorCaretEvent extends java.util.EventObject { + + private final int affectedStartOffset; + + private final int affectedEndOffset; + + EditorCaretEvent(EditorCaret source, int affectedStartOffset, int affectedEndOffset) { + super(source); + this.affectedStartOffset = affectedStartOffset; + this.affectedEndOffset = affectedEndOffset; + } + + /** + * Get caret instance to which this event relates. + * + * @return caret instance. + */ + public @NonNull EditorCaret getCaret() { + return (EditorCaret) getSource(); + } + + /** + * Get start of the region that was affected by caret change. + *
    + * This offset region will be repainted automatically by the editor infrastructure. + * + * @return >= 0 offset. + */ + public int getAffectedStartOffset() { + return affectedStartOffset; + } + + /** + * Get end of the region that was affected by caret change. + *
    + * This offset region will be repainted automatically by the editor infrastructure. + * + * @return >= 0 offset. + */ + public int getAffectedEndOffset() { + return affectedEndOffset; + } +} diff --git a/editor.lib2/src/org/netbeans/api/editor/caret/EditorCaretListener.java b/editor.lib2/src/org/netbeans/api/editor/caret/EditorCaretListener.java new file mode 100644 --- /dev/null +++ b/editor.lib2/src/org/netbeans/api/editor/caret/EditorCaretListener.java @@ -0,0 +1,61 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2015 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 2015 Sun Microsystems, Inc. + */ +package org.netbeans.api.editor.caret; + +import org.netbeans.api.annotations.common.NonNull; + +/** + * Listener for changes in editor caret. + * + * @author Miloslav Metelka + * @since 2.6 + */ +public interface EditorCaretListener extends java.util.EventListener { + + /** + * Notification about change in terms of caret addition/removal or movement. + * + * @param evt caret event describing the change. + */ + void caretChanged(@NonNull EditorCaretEvent evt); + +} diff --git a/editor.lib2/src/org/netbeans/api/editor/caret/package.html b/editor.lib2/src/org/netbeans/api/editor/caret/package.html new file mode 100644 --- /dev/null +++ b/editor.lib2/src/org/netbeans/api/editor/caret/package.html @@ -0,0 +1,158 @@ + + + + + + org.netbeans.api.editor.caret + + + +

    + The Editor Caret API opens up the editor to give information about its carets + and to manipulate them. +

    + +

    Key parts of the API

    + +

    + The whole API is organized around the + EditorCaret + class, an implementation of the javax.swing.text.Caret managing + all the carets in a single document. Information about carets maintained by + EditorCaret is stored in the immutable class + CaretInfo. + Once a caret gets mutated its corresponding caret info becomes obsolete + and new caret info instance gets created lazily. + EditorCaretListener + can be registered to an EditorCaret to be informed about caret + addition/removal or movement with an + EditorCaretEvent. +

    + +

    + The EditorCaret mutates its carets in a single transaction. + CaretMoveContext + allows clients implementing + CaretMoveHandler + to manipulate carets. The following code shows how all carets are moved to the + end of the word they are currently on. + +

    
    +    editorCaret.moveCarets((CaretMoveContext context) -> {
    +        for (CaretInfo ci : context.getOriginalCarets()) {
    +            Position pos = target.getDocument().createPosition(Utilities.getWordEnd(target, ci.getDot()));
    +            context.setDot(ci, pos);
    +        }
    +    });
    +  
    +  
    +

    + +

    Locking and Document changes

    + +

    + The basics of the locking and events model of Swing documents is + described in the javadoc of the + javax.swing.text.AbstractDocument + class. Netbeans documents use the same patterns and so does the Caret API, + because of its tight relation to documents. The fundamentals of the Swing documents + locking model are that any changes to a document are done under the + document's write lock, the document's listeners are notified synchronously on the + mutating thread and have full read access to the document, but can't modify it. +

    + +
      +
    • + Any calls from the infrastructure to EditorCaret to query or + mutate its carets, should be called with document's read-lock acquired which + will guarantee stability of CaretInfo#getDot() and + CaretInfo#getMark() and prevent caret merging as a possible + effect of document modifications. +
    • +
    • + When EditorCaret needs to notify its listeners that some + of its carets have changed, all the events have to be fired under the + layer's document's read lock. Obviously, the listeners are not allowed to + modify the document from the event notification methods. +
    • +
    + +

    + The Caret API does not use any special threads and any processing it + does is always done on the caller's thread. This means that the above described + constraints hardly cause any limitation for practical use. +

    + +

    + The Caret API is generally thread-safe meaning that it can be used + simultaneously from multiple threads if not stated otherwise. This doesn't + change in any way the rule of acquiring a read lock before calling the API. + Swing documents generally allow access for multiple readers that can run + concurrently. +

    + +

    Backwards compatibility

    +

    + As a technical limitation, EditorCaret has to implement Caret + to be able to work with Swings Text API. The Caret interface is not + aware of multiple carets and any call to its method will work on the last added + caret. Manipulating the caret using setDot(int) or moveDot(int) + will retain only the last added caret and move it accordingly. +
    + editorTextComponent.getCaret() will now always return EditorCaret + instance instead of the original caret implementation org.netbeans.editor.BaseCaret. + Since clients were expected to only rely on javax.swing.text.Caret returned type + (except internal code in editor modules) they should not be affected by the change + since EditorCaret implements Caret interface. +
    + With official Caret API the clients can start to cast + editorTextComponent.getCaret() to EditorCaret type + and use its extended functionality. +

    + +

    Use cases

    +

    + Use cases are shown in javadoc documentation of particular methods. +

    + + diff --git a/editor.lib2/src/org/netbeans/modules/editor/lib2/Bundle.properties b/editor.lib2/src/org/netbeans/modules/editor/lib2/Bundle.properties --- a/editor.lib2/src/org/netbeans/modules/editor/lib2/Bundle.properties +++ b/editor.lib2/src/org/netbeans/modules/editor/lib2/Bundle.properties @@ -47,4 +47,5 @@ #DocUtils.debugPosition wrong_position=Wrong position -MSG_RectangularSelectionClipboardFlavor=Rectangular Selection \ No newline at end of file +MSG_RectangularSelectionClipboardFlavor=Rectangular Selection +MSG_MultiCaretClipboardFlavor=Multi Caret \ No newline at end of file diff --git a/editor.lib2/src/org/netbeans/modules/editor/lib2/RectangularSelectionTransferHandler.java b/editor.lib2/src/org/netbeans/modules/editor/lib2/EditorCaretTransferHandler.java copy from editor.lib2/src/org/netbeans/modules/editor/lib2/RectangularSelectionTransferHandler.java copy to editor.lib2/src/org/netbeans/modules/editor/lib2/EditorCaretTransferHandler.java --- a/editor.lib2/src/org/netbeans/modules/editor/lib2/RectangularSelectionTransferHandler.java +++ b/editor.lib2/src/org/netbeans/modules/editor/lib2/EditorCaretTransferHandler.java @@ -1,4 +1,3 @@ - /* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * @@ -50,55 +49,61 @@ import java.util.Arrays; import java.util.List; import java.util.StringTokenizer; +import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.Icon; import javax.swing.JComponent; import javax.swing.TransferHandler; import javax.swing.text.AbstractDocument; import javax.swing.text.BadLocationException; +import javax.swing.text.Caret; import javax.swing.text.Document; import javax.swing.text.JTextComponent; import javax.swing.text.Position; +import org.netbeans.api.editor.caret.CaretInfo; +import org.netbeans.api.editor.caret.EditorCaret; import org.netbeans.lib.editor.util.swing.DocumentUtilities; import org.openide.util.Exceptions; import org.openide.util.NbBundle; /** * Clipboard transfer handler for rectangular selection. - *
    + *
    * It overrides the original transfer handler during the rectangular selection. * * @author Miloslav Metelka */ -public class RectangularSelectionTransferHandler extends TransferHandler { +public class EditorCaretTransferHandler extends TransferHandler { public static void install(JTextComponent c) { TransferHandler origHandler = c.getTransferHandler(); - if (!(origHandler instanceof RectangularSelectionTransferHandler)) { - c.setTransferHandler(new RectangularSelectionTransferHandler(c.getTransferHandler())); + if (!(origHandler instanceof EditorCaretTransferHandler)) { + c.setTransferHandler(new EditorCaretTransferHandler(c.getTransferHandler())); } } public static void uninstall(JTextComponent c) { TransferHandler origHandler = c.getTransferHandler(); - if (origHandler instanceof RectangularSelectionTransferHandler) { - c.setTransferHandler(((RectangularSelectionTransferHandler)origHandler).getDelegate()); + if (origHandler instanceof EditorCaretTransferHandler) { + c.setTransferHandler(((EditorCaretTransferHandler)origHandler).getDelegate()); } } private static final DataFlavor RECTANGULAR_SELECTION_FLAVOR = new DataFlavor(RectangularSelectionData.class, - NbBundle.getMessage(RectangularSelectionTransferHandler.class, "MSG_RectangularSelectionClipboardFlavor")); + NbBundle.getMessage(EditorCaretTransferHandler.class, "MSG_RectangularSelectionClipboardFlavor")); + + private static final DataFlavor MULTI_CARET_FLAVOR = new DataFlavor(MultiCaretData.class, + NbBundle.getMessage(EditorCaretTransferHandler.class, "MSG_MultiCaretClipboardFlavor")); /** Boolean property defining whether selection is being rectangular in a particular text component. */ private static final String RECTANGULAR_SELECTION_PROPERTY = "rectangular-selection"; // NOI18N // -J-Dorg.netbeans.modules.editor.lib2.RectangularSelectionClipboardHandler.level=FINE - private static final Logger LOG = Logger.getLogger(RectangularSelectionTransferHandler.class.getName()); - + private static final Logger LOG = Logger.getLogger(EditorCaretTransferHandler.class.getName()); private final TransferHandler delegate; - public RectangularSelectionTransferHandler(TransferHandler delegate) { + public EditorCaretTransferHandler(TransferHandler delegate) { this.delegate = delegate; } @@ -195,7 +200,7 @@ } clip.setContents( - new WrappedTransferable( + new RectangularTransferable( new StringSelection(stringSelectionBuffer.toString()), new RectangularSelectionData(data)), null); @@ -209,7 +214,68 @@ } return; - } else { // No rectangular selection + } else if (c instanceof JTextComponent && + ((JTextComponent)c).getCaret() instanceof EditorCaret && + ((EditorCaret)((JTextComponent)c).getCaret()).getCarets().size() > 1) + { + EditorCaret editorCaret = (EditorCaret) ((JTextComponent)c).getCaret(); + final JTextComponent tc = (JTextComponent) c; + StringBuilder stringSelectionBuffer; + String[] lines = null; + AbstractDocument doc = (AbstractDocument) tc.getDocument(); + doc.readLock(); + try { + CharSequence docText = DocumentUtilities.getText(doc); + stringSelectionBuffer = new StringBuilder(100); + List carets = editorCaret.getSortedCarets(); + lines = new String[carets.size()]; + boolean newline = false; + for (int i = 0; i < carets.size(); i++) { + CaretInfo caret = carets.get(i); + if(!caret.isSelection()) continue; + int startOffset = caret.getSelectionStart(); + int endOffset = caret.getSelectionEnd(); + CharSequence lineSel = docText.subSequence(startOffset, endOffset); + if (newline) { + stringSelectionBuffer.append('\n'); + } else { + newline = true; + } + stringSelectionBuffer.append(lineSel); + lines[i] = lineSel.toString(); + } + } finally { + doc.readUnlock(); + } + + clip.setContents( + new MultiCaretTransferable( + new StringSelection(stringSelectionBuffer.toString()), + new MultiCaretData(lines)), + null); + + if (action == TransferHandler.MOVE) { + List carets = editorCaret.getSortedCarets(); + for (CaretInfo caret : carets) { + if (caret.isSelection()) { + // remove selection + final int dot = caret.getDot(); + final int mark = caret.getMark(); + try { + if (RectangularSelectionUtils.isRectangularSelection(tc)) { + RectangularSelectionUtils.removeSelection(tc); + RectangularSelectionCaretAccessor.get().setRectangularSelectionToDotAndMark(editorCaret); + } else { + doc.remove(Math.min(dot, mark), Math.abs(dot - mark)); + } + } catch (BadLocationException ble) { + LOG.log(Level.FINE, null, ble); + } + } + } + } + return; + } else { // No rectangular selection or multiple carets delegate.exportToClipboard(c, clip, action); } } @@ -262,7 +328,7 @@ } else { // Regular selection String s = (String) t.getTransferData(DataFlavor.stringFlavor); // There should be string flavor if (s != null) { - tc.replaceSelection(""); + tc.replaceSelection(s); } } result = true; @@ -315,11 +381,83 @@ Exceptions.printStackTrace(ex); } } + } else { + Caret caret = ((JTextComponent) c).getCaret(); + if(caret instanceof EditorCaret && ((EditorCaret) caret).getSortedCarets().size() > 1) { + final EditorCaret editorCaret = (EditorCaret) caret; + boolean result = false; + MultiCaretData multiCaretData = null; + if(t.isDataFlavorSupported(MULTI_CARET_FLAVOR)) { + try { + multiCaretData = (MultiCaretData) t.getTransferData(MULTI_CARET_FLAVOR); + } catch (UnsupportedFlavorException | IOException ex) { + Exceptions.printStackTrace(ex); + } + } + if(multiCaretData != null && multiCaretData.strings().length == editorCaret.getCarets().size()) { + final MultiCaretData content = multiCaretData; + final Document doc = ((JTextComponent) c).getDocument(); + DocUtils.runAtomicAsUser(doc, new Runnable() { + @Override + public void run() { + for (int i = 0; i < editorCaret.getSortedCarets().size(); i++) { + CaretInfo ci = editorCaret.getSortedCarets().get(i); + try { + int startOffset = ci.getSelectionStart(); + int endOffset = ci.getSelectionEnd(); + if (startOffset != endOffset) { + doc.remove(startOffset, endOffset - startOffset); + } + if (content.strings()[i] != null && content.strings()[i].length() > 0) { + doc.insertString(startOffset, content.strings()[i], null); + } + } catch (BadLocationException ex) { + //ignore ? + } + } + } + }); + } else { + try { + final String content = (String) t.getTransferData(DataFlavor.stringFlavor); // There should be string flavor + final Document doc = ((JTextComponent) c).getDocument(); + DocUtils.runAtomicAsUser(doc, new Runnable() { + @Override + public void run() { + for (CaretInfo ci : editorCaret.getSortedCarets()) { + try { + int startOffset = ci.getSelectionStart(); + int endOffset = ci.getSelectionEnd(); + if (startOffset != endOffset) { + doc.remove(startOffset, endOffset - startOffset); + } + if (content != null && content.length() > 0) { + doc.insertString(startOffset, content, null); + } + } catch (BadLocationException ex) { + //ignore ? + } + } + } + }); + result = true; + } catch (UnsupportedFlavorException | IOException ex) { + Exceptions.printStackTrace(ex); + } + } + + return result; + } } } return delegate.importData(support); // Regular importData() } + @Override + public boolean importData(JComponent comp, Transferable t) { + return delegate.importData(comp, t); + } + private static List splitByLines(String s) { StringTokenizer splitByLines = new StringTokenizer(s, "\n", false); List lines = new ArrayList(); @@ -329,7 +467,7 @@ return lines; } - private static final class WrappedTransferable implements Transferable { + private static final class RectangularTransferable implements Transferable { private final Transferable delegate; @@ -337,7 +475,7 @@ private DataFlavor[] transferDataFlavorsCache; - public WrappedTransferable(Transferable delegate, RectangularSelectionData rectangularSelectionData) { + public RectangularTransferable(Transferable delegate, RectangularSelectionData rectangularSelectionData) { this.delegate = delegate; this.rectangularSelectionData = rectangularSelectionData; } @@ -367,6 +505,61 @@ return delegate.getTransferData(flavor); } } + + private static final class MultiCaretTransferable implements Transferable { + + private final Transferable delegate; + + private final MultiCaretData multiCaretData; + + private DataFlavor[] transferDataFlavorsCache; + + public MultiCaretTransferable(Transferable delegate, MultiCaretData multiCaretData) { + this.delegate = delegate; + this.multiCaretData = multiCaretData; + } + + @Override + public synchronized DataFlavor[] getTransferDataFlavors() { + if (transferDataFlavorsCache != null) { + return transferDataFlavorsCache; + } + DataFlavor[] flavors = delegate.getTransferDataFlavors(); + DataFlavor[] result = Arrays.copyOf(flavors, flavors.length + 1); + result[flavors.length] = MULTI_CARET_FLAVOR; + + return transferDataFlavorsCache = result; + } + + @Override + public boolean isDataFlavorSupported(DataFlavor flavor) { + return MULTI_CARET_FLAVOR.equals(flavor) || delegate.isDataFlavorSupported(flavor); + } + + @Override + public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException { + if (MULTI_CARET_FLAVOR.equals(flavor)) { + return multiCaretData; + } + return delegate.getTransferData(flavor); + } + } + + public static final class MultiCaretData { + + private final String[] strings; + + public MultiCaretData(String[] strings) { + this.strings = strings; + } + + /** + * Strings containing rectangular selection (on particular selected line). + */ + public String[] strings() { + return strings; + } + } public static final class RectangularSelectionData { diff --git a/editor.lib2/src/org/netbeans/modules/editor/lib2/EditorPreferencesDefaults.java b/editor.lib2/src/org/netbeans/modules/editor/lib2/EditorPreferencesDefaults.java --- a/editor.lib2/src/org/netbeans/modules/editor/lib2/EditorPreferencesDefaults.java +++ b/editor.lib2/src/org/netbeans/modules/editor/lib2/EditorPreferencesDefaults.java @@ -58,7 +58,19 @@ // no-op } - public static final String LINE_CARET = "line-caret"; + /** + * One dot thin line compatible with Swing default caret. + */ + public static final String THIN_LINE_CARET = "thin-line-caret"; // NOI18N + + /** + * Two dots line - the default for the editor. + */ + public static final String THICK_LINE_CARET = "thick-line-caret"; // NOI18N + + /** + * Rectangle covering character to possibly be overwritten. + */ public static final String BLOCK_CARET = "block-caret"; public static final boolean defaultToolbarVisible = true; // Currently unused - see ToggleAction in editor.actions @@ -100,12 +112,12 @@ public static final boolean defaultExpandTabs = true; - public static final String defaultCaretTypeInsertMode = LINE_CARET; + public static final String defaultCaretTypeInsertMode = THICK_LINE_CARET; public static final String defaultCaretTypeOverwriteMode = BLOCK_CARET; public static final boolean defaultCaretItalicInsertMode = false; public static final boolean defaultCaretItalicOverwriteMode = false; /** @since 1.23 */ - public static final int defaultThickCaretWidth = 5; + public static final int defaultThickCaretWidth = 2; public static final Acceptor defaultAbbrevExpandAcceptor = AcceptorFactory.WHITESPACE; public static final Acceptor defaultAbbrevAddTypedCharAcceptor = AcceptorFactory.NL; public static final Acceptor defaultAbbrevResetAcceptor = AcceptorFactory.NON_JAVA_IDENTIFIER; diff --git a/editor.lib2/src/org/netbeans/modules/editor/lib2/RectangularSelectionCaretAccessor.java b/editor.lib2/src/org/netbeans/modules/editor/lib2/RectangularSelectionCaretAccessor.java new file mode 100644 --- /dev/null +++ b/editor.lib2/src/org/netbeans/modules/editor/lib2/RectangularSelectionCaretAccessor.java @@ -0,0 +1,77 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2016 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 2016 Sun Microsystems, Inc. + */ +package org.netbeans.modules.editor.lib2; + +import org.netbeans.api.editor.caret.EditorCaret; + +/** + * Accessor of EditorCaret's rectangular selection methods. + * + * @author Miloslav Metelka + */ +public abstract class RectangularSelectionCaretAccessor { + + private static RectangularSelectionCaretAccessor INSTANCE; + + public static RectangularSelectionCaretAccessor get() { + if (INSTANCE == null) { + // Cause api accessor impl to get initialized + try { + Class.forName(EditorCaret.class.getName(), true, RectangularSelectionCaretAccessor.class.getClassLoader()); + } catch (ClassNotFoundException e) { + // Should never happen + } + } + return INSTANCE; + } + + public static void register(RectangularSelectionCaretAccessor accessor) { + INSTANCE = accessor; + } + + public abstract void setRectangularSelectionToDotAndMark(EditorCaret editorCaret); + + public abstract void updateRectangularUpDownSelection(EditorCaret editorCaret); + + public abstract void extendRectangularSelection(EditorCaret editorCaret, boolean toRight, boolean ctrl); + +} diff --git a/editor.lib2/src/org/netbeans/modules/editor/lib2/RectangularSelectionTransferHandler.java b/editor.lib2/src/org/netbeans/modules/editor/lib2/RectangularSelectionTransferHandler.java --- a/editor.lib2/src/org/netbeans/modules/editor/lib2/RectangularSelectionTransferHandler.java +++ b/editor.lib2/src/org/netbeans/modules/editor/lib2/RectangularSelectionTransferHandler.java @@ -65,11 +65,13 @@ /** * Clipboard transfer handler for rectangular selection. - *
    + *
    * It overrides the original transfer handler during the rectangular selection. * * @author Miloslav Metelka + * @deprecated replaced by {@link EditorCaretTransferHandler}, kept in place for BaseCaret */ +@Deprecated public class RectangularSelectionTransferHandler extends TransferHandler { public static void install(JTextComponent c) { diff --git a/editor.lib2/src/org/netbeans/modules/editor/lib2/WeakReferenceStableList.java b/editor.lib2/src/org/netbeans/modules/editor/lib2/WeakReferenceStableList.java --- a/editor.lib2/src/org/netbeans/modules/editor/lib2/WeakReferenceStableList.java +++ b/editor.lib2/src/org/netbeans/modules/editor/lib2/WeakReferenceStableList.java @@ -50,7 +50,7 @@ /** * List holding non-null items weakly with automatic collection of GCed items. - *
    + *
    * Add and remove operations do not affect a list previously obtained by {@link #getList() } * so it may be used in a similar way like ListenerList holding all listener instances * by a weak reference. diff --git a/editor.lib2/src/org/netbeans/modules/editor/lib2/actions/EditorActionUtilities.java b/editor.lib2/src/org/netbeans/modules/editor/lib2/actions/EditorActionUtilities.java --- a/editor.lib2/src/org/netbeans/modules/editor/lib2/actions/EditorActionUtilities.java +++ b/editor.lib2/src/org/netbeans/modules/editor/lib2/actions/EditorActionUtilities.java @@ -382,7 +382,7 @@ /** * Get single-key accelerator for a given declared action. - *
    + *
    * Unfortunately currently there's no easy way to display multi-keybinding in menu-item * (there's just JMenuItem.setAccelerator() and its impl is L&F-based) * so just display single-keystroke accelerators. diff --git a/editor.lib2/src/org/netbeans/modules/editor/lib2/actions/PresenterEditorAction.java b/editor.lib2/src/org/netbeans/modules/editor/lib2/actions/PresenterEditorAction.java --- a/editor.lib2/src/org/netbeans/modules/editor/lib2/actions/PresenterEditorAction.java +++ b/editor.lib2/src/org/netbeans/modules/editor/lib2/actions/PresenterEditorAction.java @@ -73,7 +73,7 @@ /** * Action that represents a named editor action in main menu, popup menu * and editor toolbar. - *
    + *
    * The actions are registered into "Editors/ActionPresenters" regardless * of the mime-type for which the actions get created. */ diff --git a/editor.lib2/src/org/netbeans/modules/editor/lib2/actions/PresenterUpdater.java b/editor.lib2/src/org/netbeans/modules/editor/lib2/actions/PresenterUpdater.java --- a/editor.lib2/src/org/netbeans/modules/editor/lib2/actions/PresenterUpdater.java +++ b/editor.lib2/src/org/netbeans/modules/editor/lib2/actions/PresenterUpdater.java @@ -106,7 +106,7 @@ /** * For menu presenters hold action of last focused editor component. - *
    + *
    * Hold reference strongly but may be changed to weak reference in case * even actions of last activated text component are desired to be released. */ diff --git a/editor.lib2/src/org/netbeans/modules/editor/lib2/highlighting/AbstractOffsetGapList.java b/editor.lib2/src/org/netbeans/modules/editor/lib2/highlighting/AbstractOffsetGapList.java --- a/editor.lib2/src/org/netbeans/modules/editor/lib2/highlighting/AbstractOffsetGapList.java +++ b/editor.lib2/src/org/netbeans/modules/editor/lib2/highlighting/AbstractOffsetGapList.java @@ -643,11 +643,11 @@ /** * This method updates element's offset (shifts it below offset gap if necessary) * before (or after) the element gets removed from the list. - *
    + *
    * This method should be called after the element is physically removed * from the list and it's desired that it retains its natural offset * (not possibly shifted by the offset gap length). - *
    + *
    * If the element was located below the offset gap prior removal * then calling of this method is not necessary. */ diff --git a/editor.lib2/src/org/netbeans/modules/editor/lib2/highlighting/CaretBasedBlockHighlighting.java b/editor.lib2/src/org/netbeans/modules/editor/lib2/highlighting/CaretBasedBlockHighlighting.java --- a/editor.lib2/src/org/netbeans/modules/editor/lib2/highlighting/CaretBasedBlockHighlighting.java +++ b/editor.lib2/src/org/netbeans/modules/editor/lib2/highlighting/CaretBasedBlockHighlighting.java @@ -64,18 +64,19 @@ import javax.swing.text.Position; import javax.swing.text.SimpleAttributeSet; import javax.swing.text.StyleConstants; +import org.netbeans.api.editor.caret.CaretInfo; +import org.netbeans.api.editor.caret.EditorCaret; import org.netbeans.api.editor.mimelookup.MimeLookup; import org.netbeans.api.editor.mimelookup.MimePath; import org.netbeans.api.editor.settings.AttributesUtilities; import org.netbeans.api.editor.settings.FontColorNames; import org.netbeans.api.editor.settings.FontColorSettings; -import org.netbeans.lib.editor.util.swing.BlockCompare; import org.netbeans.modules.editor.lib2.DocUtils; import org.netbeans.modules.editor.lib2.RectangularSelectionUtils; import org.netbeans.spi.editor.highlighting.HighlightsChangeEvent; import org.netbeans.spi.editor.highlighting.HighlightsChangeListener; +import org.netbeans.spi.editor.highlighting.HighlightsContainer; import org.netbeans.spi.editor.highlighting.HighlightsSequence; -import org.netbeans.spi.editor.highlighting.support.AbstractHighlightsContainer; import org.netbeans.spi.editor.highlighting.support.PositionsBag; import org.openide.util.Lookup; import org.openide.util.LookupEvent; @@ -87,7 +88,7 @@ * * @author Vita Stejskal */ -public abstract class CaretBasedBlockHighlighting extends AbstractHighlightsContainer implements ChangeListener, PropertyChangeListener { +public abstract class CaretBasedBlockHighlighting implements HighlightsContainer, ChangeListener, PropertyChangeListener { // -J-Dorg.netbeans.modules.editor.lib2.highlighting.CaretBasedBlockHighlighting.level=FINE private static final Logger LOG = Logger.getLogger(CaretBasedBlockHighlighting.class.getName()); @@ -103,8 +104,7 @@ private final boolean extendsEOL; private final boolean extendsEmptyLine; - private Position currentBlockStart; - private Position currentBlockEnd; + private final PositionsBag selectionsBag; private AttributeSet attribs; @@ -122,6 +122,8 @@ // Hook up the component this.component = component; + + selectionsBag = new PositionsBag(component.getDocument(), false); } private void init() { @@ -156,31 +158,18 @@ inited = true; init(); } + + return selectionsBag.getHighlights(startOffset, endOffset); + } - Position blockStart; - Position blockEnd; - synchronized (this) { - blockStart = currentBlockStart; - blockEnd = currentBlockEnd; - } - if (blockStart != null && blockEnd != null && - endOffset >= blockStart.getOffset() && startOffset <= blockEnd.getOffset()) - { - if (LOG.isLoggable(Level.FINE)) { - LOG.fine("Queried for highlights in <" //NOI18N - + startOffset + ", " + endOffset + ">, returning <" //NOI18N - + positionToString(blockStart) + ", " + positionToString(blockEnd) + ">" //NOI18N - + ", layer=" + s2s(this) + '\n'); //NOI18N - } + @Override + public void addHighlightsChangeListener(HighlightsChangeListener listener) { + selectionsBag.addHighlightsChangeListener(listener); + } - return new SimpleHighlightsSequence( - Math.max(blockStart.getOffset(), startOffset), - Math.min(blockEnd.getOffset(), endOffset), - getAttribs() - ); - } else { - return HighlightsSequence.EMPTY; - } + @Override + public void removeHighlightsChangeListener(HighlightsChangeListener listener) { + selectionsBag.removeHighlightsChangeListener(listener); } // ------------------------------------------------ @@ -215,7 +204,7 @@ updateLineInfo(true); } - protected abstract Position [] getCurrentBlockPositions(Document document); + protected abstract PositionsBag getCurrentBlockPositions(Document document); // ------------------------------------------------ // private implementation @@ -224,78 +213,12 @@ private final void updateLineInfo(boolean fire) { ((AbstractDocument) component.getDocument()).readLock(); try { - Position newStart; - Position newEnd; - Position changeStart; - Position changeEnd; - Position[] newBlock = getCurrentBlockPositions(component.getDocument()); + PositionsBag newPositions = getCurrentBlockPositions(component.getDocument()); synchronized (this) { - if (newBlock != null) { - newStart = newBlock[0]; - newEnd = newBlock[1]; - if (currentBlockStart == null) { // not valid yet - if (newStart.getOffset() < newEnd.getOffset()) { // Valid non-empty block - changeStart = newStart; - changeEnd = newEnd; - } else { - changeStart = null; // Invalid or empty block => no change - changeEnd = null; - } - } else { // Valid current start and end blocks - // Compare new block to old one - BlockCompare compare = BlockCompare.get( - newStart.getOffset(), newEnd.getOffset(), - currentBlockStart.getOffset(), currentBlockEnd.getOffset()); - if (compare.invalidX()) { // newStart > newEnd - changeStart = null; // No change - changeEnd = null; - } else if (compare.equal()) { // Same blocks - changeStart = null; // No firing - changeEnd = null; - } else if (compare.equalStart()) { - if (compare.containsStrict()) { - changeStart = currentBlockEnd; - changeEnd = newEnd; - } else { - assert (compare.insideStrict()); - changeStart = newEnd; - changeEnd = currentBlockEnd; - } - } else if (compare.equalEnd()) { - if (compare.containsStrict()) { - changeStart = newStart; - changeEnd = currentBlockStart; - } else { - assert (compare.insideStrict()); - changeStart = currentBlockStart; - changeEnd = newStart; - } - } else { - changeStart = (compare.lowerStart()) ? newStart : currentBlockStart; - changeEnd = (compare.lowerEnd()) ? currentBlockEnd : newEnd; - } - } - + if (newPositions != null) { + selectionsBag.setHighlights(newPositions); } else { // newBlock is null => selection removed - newStart = null; - newEnd = null; - changeStart = currentBlockStart; - changeEnd = currentBlockEnd; - } - - if (LOG.isLoggable(Level.FINE)) { - LOG.fine("Caret selection block changed from [" //NOI18N - + positionToString(currentBlockStart) + ", " + positionToString(currentBlockEnd) + "] to [" //NOI18N - + positionToString(newStart) + ", " + positionToString(newEnd) + "]" //NOI18N - + ", layer=" + s2s(this) + '\n'); //NOI18N - } - currentBlockStart = newStart; - currentBlockEnd = newEnd; - } - - if (changeStart != null) { - if (fire) { - fireHighlightsChange(changeStart.getOffset(), changeEnd.getOffset()); + selectionsBag.clear(); } } } finally { @@ -356,46 +279,6 @@ private static String s2s(Object o) { return o == null ? "null" : o.getClass().getSimpleName() + "@" + Integer.toHexString(System.identityHashCode(o)); //NOI18N } - - public static final class SimpleHighlightsSequence implements HighlightsSequence { - - private int startOffset; - private int endOffset; - private AttributeSet attribs; - - private boolean end = false; - - public SimpleHighlightsSequence(int startOffset, int endOffset, AttributeSet attribs) { - this.startOffset = startOffset; - this.endOffset = endOffset; - this.attribs = attribs; - } - - @Override - public boolean moveNext() { - if (!end) { - end = true; - return true; - } else { - return false; - } - } - - @Override - public int getStartOffset() { - return startOffset; - } - - @Override - public int getEndOffset() { - return endOffset; - } - - @Override - public AttributeSet getAttributes() { - return attribs; - } - } // End of SimpleHighlightsSequence public static final class CaretRowHighlighting extends CaretBasedBlockHighlighting { @@ -405,27 +288,46 @@ super(component, FontColorNames.CARET_ROW_COLORING, true, false); } - protected Position[] getCurrentBlockPositions(Document document) { + @Override + protected PositionsBag getCurrentBlockPositions(Document document) { Caret caret = caret(); + PositionsBag selections = null; if (document != null && caret != null) { - int caretOffset = caret.getDot(); - try { - int startOffset = DocUtils.getRowStart(document, caretOffset, 0); - int endOffset = DocUtils.getRowEnd(document, caretOffset); + selections = new PositionsBag(document); + if(caret instanceof EditorCaret) { + EditorCaret editorCaret = (EditorCaret) caret; + List carets = editorCaret.getCarets(); + for (CaretInfo c : carets) { + int caretOffset = c.getDot(); + try { + int startOffset = DocUtils.getRowStart(document, caretOffset, 0); + int endOffset = DocUtils.getRowEnd(document, caretOffset); + // include the new-line character or the end of the document + endOffset++; + Position startPos = document.createPosition(startOffset); + Position endPos = document.createPosition(endOffset); + selections.addHighlight(startPos, endPos, getAttribs()); + } catch(BadLocationException e) { + LOG.log(Level.WARNING, e.getMessage(), e); + } + } + } else { + int caretOffset = caret.getDot(); + try { + int startOffset = DocUtils.getRowStart(document, caretOffset, 0); + int endOffset = DocUtils.getRowEnd(document, caretOffset); - // include the new-line character or the end of the document - endOffset++; - - return new Position [] { - document.createPosition(startOffset), - document.createPosition(endOffset), - }; - } catch (BadLocationException e) { - LOG.log(Level.WARNING, e.getMessage(), e); + // include the new-line character or the end of the document + endOffset++; + Position startPos = document.createPosition(startOffset); + Position endPos = document.createPosition(endOffset); + selections.addHighlight(startPos, endPos, getAttribs()); + } catch (BadLocationException e) { + LOG.log(Level.WARNING, e.getMessage(), e); + } } } - - return null; + return selections; } } // End of CaretRowHighlighting class @@ -445,43 +347,62 @@ } @Override - protected Position[] getCurrentBlockPositions(Document document) { + protected PositionsBag getCurrentBlockPositions(Document document) { Caret caret = caret(); + PositionsBag selections = null; if (document != null && caret != null) { - int caretOffset = caret.getDot(); - int markOffset = caret.getMark(); + selections = new PositionsBag(document); + if(caret instanceof EditorCaret) { + EditorCaret editorCaret = (EditorCaret) caret; + for (CaretInfo caretInfo : editorCaret.getCarets()) { + int caretOffset = caretInfo.getDot(); + int markOffset = caretInfo.getMark(); - if (caretOffset != markOffset) { - try { - return new Position [] { - document.createPosition(Math.min(caretOffset, markOffset)), - document.createPosition(Math.max(caretOffset, markOffset)), - }; - } catch (BadLocationException e) { - LOG.log(Level.WARNING, e.getMessage(), e); + if (caretOffset != markOffset) { + try { + Position start = document.createPosition(Math.min(caretOffset, markOffset)); + Position end = document.createPosition(Math.max(caretOffset, markOffset)); + selections.addHighlight(start, end, getAttribs()); + } catch (BadLocationException e) { + LOG.log(Level.WARNING, e.getMessage(), e); + } + } + } + } else { + int caretOffset = caret.getDot(); + int markOffset = caret.getMark(); + + if (caretOffset != markOffset) { + try { + Position start = document.createPosition(Math.min(caretOffset, markOffset)); + Position end = document.createPosition(Math.max(caretOffset, markOffset)); + selections.addHighlight(start, end, getAttribs()); + } catch (BadLocationException e) { + LOG.log(Level.WARNING, e.getMessage(), e); + } } } } - return null; + return selections; } - @Override - public HighlightsSequence getHighlights(int startOffset, int endOffset) { - if (!RectangularSelectionUtils.isRectangularSelection(component())) { // regular selection - return super.getHighlights(startOffset, endOffset); - } else { // rectangular selection - return (rectangularSelectionBag != null) - ? (rectangularSelectionBag.getHighlights(startOffset, endOffset)) - : HighlightsSequence.EMPTY; - } - } +// @Override +// public HighlightsSequence getHighlights(int startOffset, int endOffset) { +// if (!RectangularSelectionUtils.isRectangularSelection(component())) { // regular selection +// return super.getHighlights(startOffset, endOffset); +// } else { // rectangular selection +// return (rectangularSelectionBag != null) +// ? (rectangularSelectionBag.getHighlights(startOffset, endOffset)) +// : HighlightsSequence.EMPTY; +// } +// } @Override public void propertyChange(PropertyChangeEvent evt) { super.propertyChange(evt); if (RectangularSelectionUtils.getRectangularSelectionProperty().equals(evt.getPropertyName())) { - fireHighlightsChange(0, component().getDocument().getLength()); +// fireHighlightsChange(0, component().getDocument().getLength()); } } @@ -508,7 +429,7 @@ } // Fire change at once if (hlChangeStartOffset != -1) { - fireHighlightsChange(hlChangeStartOffset, hlChangeEndOffset); +// fireHighlightsChange(hlChangeStartOffset, hlChangeEndOffset); hlChangeStartOffset = -1; } } diff --git a/editor.lib2/src/org/netbeans/modules/editor/lib2/highlighting/CaretOverwriteModeHighlighting.java b/editor.lib2/src/org/netbeans/modules/editor/lib2/highlighting/CaretOverwriteModeHighlighting.java new file mode 100644 --- /dev/null +++ b/editor.lib2/src/org/netbeans/modules/editor/lib2/highlighting/CaretOverwriteModeHighlighting.java @@ -0,0 +1,351 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2015 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 2015 Sun Microsystems, Inc. + */ +package org.netbeans.modules.editor.lib2.highlighting; + +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.swing.text.AttributeSet; +import javax.swing.text.Caret; +import javax.swing.text.Document; +import javax.swing.text.JTextComponent; +import javax.swing.text.StyleConstants; +import org.netbeans.api.editor.caret.CaretInfo; +import org.netbeans.api.editor.caret.EditorCaret; +import org.netbeans.api.editor.caret.EditorCaretEvent; +import org.netbeans.api.editor.caret.EditorCaretListener; +import org.netbeans.api.editor.settings.AttributesUtilities; +import org.netbeans.lib.editor.util.ListenerList; +import org.netbeans.lib.editor.util.swing.DocumentUtilities; +import org.netbeans.spi.editor.highlighting.HighlightsChangeEvent; +import org.netbeans.spi.editor.highlighting.HighlightsChangeListener; +import org.netbeans.spi.editor.highlighting.HighlightsSequence; +import org.netbeans.spi.editor.highlighting.ReleasableHighlightsContainer; +import org.netbeans.spi.editor.highlighting.ShiftHighlightsSequence; +import org.openide.util.WeakListeners; + +/** + * Used by EditorCaret to color a single character under the block caret(s) with inverse colors in + * overwrite mode when the caret blinking timer ticks. + * + * @author Miloslav Metelka + */ +public final class CaretOverwriteModeHighlighting implements ReleasableHighlightsContainer, PropertyChangeListener, EditorCaretListener { + + public static final String LAYER_TYPE_ID = "org.netbeans.modules.editor.lib2.highlighting.CaretOverwriteModeHighlighting"; //NOI18N + + // -J-Dorg.netbeans.modules.editor.lib2.highlighting.CaretOverwriteModeHighlighting.level=FINE + private static final Logger LOG = Logger.getLogger(CaretOverwriteModeHighlighting.class.getName()); + + private final JTextComponent component; + + private boolean inited; + + private boolean visible; + + private EditorCaret editorCaret; + + private EditorCaretListener weakEditorCaretListener; + + private ListenerList listenerList = new ListenerList<>(); + + private AttributeSet coloringAttrs; + + private CharSequence docText; + + private List sortedCarets; + + /** Creates a new instance of CaretSelectionLayer */ + public CaretOverwriteModeHighlighting(JTextComponent component) { + this.component = component; + component.putClientProperty(CaretOverwriteModeHighlighting.class, this); + } + + public void setVisible(boolean visible) { + if (visible != this.visible) { + this.visible = visible; + if (editorCaret != null) { + List sortedCaretsL; + if (visible) { + sortedCaretsL = editorCaret.getSortedCarets(); + synchronized (this) { + sortedCarets = sortedCaretsL; + } + } else { + synchronized (this) { + sortedCaretsL = sortedCarets; + sortedCarets = null; + } + } + if (sortedCaretsL != null) { + int changeStartOffset = sortedCaretsL.get(0).getDot(); + int changeEndOffset = sortedCaretsL.get(sortedCaretsL.size() - 1).getDot() + 1; + fireHighlightsChange(changeStartOffset, changeEndOffset); + } + } + } + } + + private void init() { + component.addPropertyChangeListener(WeakListeners.propertyChange(this, component)); + updateActiveCaret(); + updateColoring(); + } + + // ------------------------------------------------ + // AbstractHighlightsContainer implementation + // ------------------------------------------------ + + @Override + public HighlightsSequence getHighlights(int startOffset, int endOffset) { + if (!inited) { + inited = true; + init(); + } + HighlightsSequence hs; + boolean visibleL; + List sortedCaretsL; + synchronized (this) { + visibleL = visible; + sortedCaretsL = sortedCarets; + } + if (editorCaret != null && visibleL) { + hs = new HS(sortedCaretsL, startOffset, endOffset); + if (LOG.isLoggable(Level.FINE)) { + LOG.fine("CaretOverwriteModeHighlighting.getHighlights() <" + startOffset + "," + endOffset + ">\n"); + } + } else { + hs = HighlightsSequence.EMPTY; + } + return hs; + } + + @Override + public void addHighlightsChangeListener(HighlightsChangeListener listener) { + listenerList.add(listener); + } + + @Override + public void removeHighlightsChangeListener(HighlightsChangeListener listener) { + listenerList.remove(listener); + } + + private void fireHighlightsChange(HighlightsChangeEvent evt) { + for (HighlightsChangeListener listener : listenerList.getListeners()) { + listener.highlightChanged(evt); + } + } + + private void fireHighlightsChange(int startOffset, int endOffset) { + fireHighlightsChange(new HighlightsChangeEvent(this, startOffset, endOffset)); + } + + // ------------------------------------------------ + // PropertyChangeListener implementation + // ------------------------------------------------ + + @Override + public void propertyChange(PropertyChangeEvent evt) { + String propName = evt.getPropertyName(); + if (propName == null || "caret".equals(propName)) { //NOI18N + updateActiveCaret(); + } else if ("document".equals(propName)) { + updateDocText(); + } else if ("caretColor".equals(propName) || "background".equals(propName)) { + updateColoring(); + } + } + + @Override + public void caretChanged(EditorCaretEvent evt) { + if (visible) { + fireHighlightsChange(evt.getAffectedStartOffset(), evt.getAffectedEndOffset()); + List sortedCaretsL; + sortedCaretsL = editorCaret.getSortedCarets(); + synchronized (this) { + sortedCarets = sortedCaretsL; + } + } + } + + @Override + public void released() { + component.removePropertyChangeListener(this); + } + + private void updateActiveCaret() { + if (visible) { + setVisible(false); + } + if (editorCaret != null) { + editorCaret.removeEditorCaretListener(weakEditorCaretListener); + weakEditorCaretListener = null; + editorCaret = null; + docText = null; + } + + Caret caret = component.getCaret(); + if (caret instanceof EditorCaret) { // Only work for editor caret + editorCaret = (EditorCaret) caret; + } + + if (editorCaret != null) { + updateDocText(); + weakEditorCaretListener = WeakListeners.create(EditorCaretListener.class, this, editorCaret); + editorCaret.addEditorCaretListener(weakEditorCaretListener); + } + } + + private void updateColoring() { + coloringAttrs = AttributesUtilities.createImmutable( + StyleConstants.Background, component.getCaretColor(), + StyleConstants.Foreground, component.getBackground()); + } + + private void updateDocText() { + JTextComponent c = component; + CharSequence text = null; + if (c != null) { + Document doc = c.getDocument(); + if (doc != null) { + text = DocumentUtilities.getText(doc); + } + } + docText = text; + } + + private final class HS implements ShiftHighlightsSequence { + + private final List sortedCarets; + + private final int startOffset; + + private final int endOffset; + + private int caretOffset = -1; + + private int caretOffsetEndShift; + + private int caretIndex; + + HS(List sortedCarets, int startOffset, int endOffset) { + this.sortedCarets = sortedCarets; + this.startOffset = startOffset; + this.endOffset = endOffset; + } + + @Override + public boolean moveNext() { + boolean ret = false; + if (caretOffset == -1) { // Return first highlight + while (caretOffset < startOffset && caretIndex < sortedCarets.size()) { + CaretInfo caret = sortedCarets.get(caretIndex++); + caretOffset = caret.getDot(); + } + if (caretOffset != -1) { + ret = true; + } + } else { + while (caretIndex < sortedCarets.size()) { + CaretInfo caret = sortedCarets.get(caretIndex++); + int offset = caret.getDot(); + // Check for case if sorted carets would not be truly sorted or there would be duplicates + if (offset > caretOffset) { + caretOffset = offset; + if (LOG.isLoggable(Level.FINE)) { + LOG.fine(" CaretOverwriteModeHighlighting.Highlight <" + caretOffset + "," + (caretOffset + 1) + ">\n"); + } + return true; + } + if (offset >= endOffset) { + return false; + } + } + } + if (ret) { + caretOffsetEndShift = 0; + CharSequence text = docText; + if (text != null) { + char ch = text.charAt(caretOffset); + if (ch == '\t' || ch == '\n') { + caretOffsetEndShift = 1; + } + } + if (LOG.isLoggable(Level.FINE)) { + LOG.fine(" CaretOverwriteModeHighlighting.Highlight <" + caretOffset + "(sh=0)," + + (caretOffset + 1) + "(sh=" + caretOffsetEndShift + ")>\n"); + } + return true; + } + return false; + } + + @Override + public int getStartOffset() { + return caretOffset; + } + + @Override + public int getEndOffset() { + return caretOffset + 1; + } + + @Override + public AttributeSet getAttributes() { + return coloringAttrs; + } + + @Override + public int getStartShift() { + return 0; + } + + @Override + public int getEndShift() { + return caretOffsetEndShift; + } + + + } +} diff --git a/editor.lib2/src/org/netbeans/modules/editor/lib2/highlighting/CompoundAttributes.java b/editor.lib2/src/org/netbeans/modules/editor/lib2/highlighting/CompoundAttributes.java --- a/editor.lib2/src/org/netbeans/modules/editor/lib2/highlighting/CompoundAttributes.java +++ b/editor.lib2/src/org/netbeans/modules/editor/lib2/highlighting/CompoundAttributes.java @@ -50,9 +50,9 @@ /** * Multiple highlights for a particular view contains an array of highlights * and a starting offset to which the (ending) offsets in HighlightItem(s) are related. - *
    + *
    * The view should assign this object to its internal 'attributes' variable that holds the AttributeSet. - *
    + *
    * The AttributeSet implementation returns attributes of the first section of the compound attributes. * * @author Miloslav Metelka diff --git a/editor.lib2/src/org/netbeans/modules/editor/lib2/highlighting/DirectMergeContainer.java b/editor.lib2/src/org/netbeans/modules/editor/lib2/highlighting/DirectMergeContainer.java --- a/editor.lib2/src/org/netbeans/modules/editor/lib2/highlighting/DirectMergeContainer.java +++ b/editor.lib2/src/org/netbeans/modules/editor/lib2/highlighting/DirectMergeContainer.java @@ -57,14 +57,18 @@ import org.netbeans.spi.editor.highlighting.HighlightsContainer; import org.netbeans.spi.editor.highlighting.HighlightsSequence; import org.netbeans.spi.editor.highlighting.ReleasableHighlightsContainer; +import org.netbeans.spi.editor.highlighting.ShiftHighlightsSequence; import org.openide.util.WeakListeners; /** * Compound highlights-layer container that does non-cached direct merging * of individual layers' highlights. - *
    + *
    * It's somewhat similar to a view building process in view hierarchy which also maintains * next-change-offset where a change in the particular layer occurs and needs to be processed. + *
    + * {@link ShiftHighlightsSequence} are supported and the highlights sequences returned by the container + * are always instances of this interface. * * @author Miloslav Metelka */ @@ -124,9 +128,9 @@ layerEvent = event; try { if (!listeners.isEmpty()) { - event = new HighlightsChangeEvent(this, event.getStartOffset(), event.getEndOffset()); + HighlightsChangeEvent thisEvt = new HighlightsChangeEvent(this, event.getStartOffset(), event.getEndOffset()); for (HighlightsChangeListener l : listeners) { - l.highlightChanged(event); + l.highlightChanged(thisEvt); } } synchronized (activeHlSeqs) { @@ -146,7 +150,7 @@ /** * Get event from a contained layer which caused highlight change * (mainly for debugging purposes). - *
    + *
    * The information is only available during firing to change listeners registered by * {@link #addHighlightsChangeListener(org.netbeans.spi.editor.highlighting.HighlightsChangeListener)}. * @@ -177,7 +181,7 @@ } } - static final class HlSequence implements HighlightsSequenceEx { + static final class HlSequence implements ShiftHighlightsSequence { /** * Wrappers around layers used to compute merged highlights. @@ -186,12 +190,16 @@ private int topWrapperIndex; - private final int endOffset; + final int endOffset; int mergedHighlightStartOffset; + int mergedHighlightStartShift; + int mergedHighlightEndOffset; + int mergedHighlightEndShift; + AttributeSet mergedAttrs; volatile boolean finished; // Either no more highlights or layers were changed; @@ -205,37 +213,39 @@ for (int i = 0; i < layers.length; i++) { HighlightsContainer container = layers[i]; HighlightsSequence hlSequence = container.getHighlights(startOffset, endOffset); - Wrapper wrapper = new Wrapper(container, hlSequence, startOffset); - if (!wrapper.isFinished()) { // For no-highlight wrapper do not include it at all in the array + Wrapper wrapper = new Wrapper(container, hlSequence, endOffset); + if (wrapper.init(startOffset)) { // For no-highlight wrapper do not include it at all in the array wrappers[topWrapperIndex++] = wrapper; } } topWrapperIndex--; - updateMergeVars(-1, startOffset); // Update all layers to fetch correct values + updateMergeVars(-1, startOffset, 0); // Update all layers to fetch correct values } @Override public boolean moveNext() { - if (finished || topWrapperIndex < 0) { + if (finished) { return false; } Wrapper topWrapper; - int lastHighlightEndOffset = mergedHighlightEndOffset; - while ((topWrapper = nextMerge(lastHighlightEndOffset)) != null) { - int nextChangeOffset = topWrapper.mNextChangeOffset; - if (nextChangeOffset <= lastHighlightEndOffset) { // No advance in change offset => Finished - finished = true; - return false; - } + int nextHighlightStartOffset = mergedHighlightEndOffset; + int nextHighlightStartShift = mergedHighlightEndShift; + while ((topWrapper = nextMerge(nextHighlightStartOffset, nextHighlightStartShift)) != null) { + int nextChangeOffset = topWrapper.mergedNextChangeOffset; + int nextChangeShift = topWrapper.mergedNextChangeShift; AttributeSet attrs = topWrapper.mAttrs; - if (attrs != null) { - mergedHighlightStartOffset = lastHighlightEndOffset; + if (attrs != null) { // Do not return regions with empty attrs (they are not highlights) + mergedHighlightStartOffset = nextHighlightStartOffset; + mergedHighlightStartShift = nextHighlightStartShift; mergedHighlightEndOffset = nextChangeOffset; + mergedHighlightEndShift = nextChangeShift; mergedAttrs = attrs; return true; } - lastHighlightEndOffset = nextChangeOffset; + nextHighlightStartOffset = nextChangeOffset; + nextHighlightStartShift = nextChangeShift; } + finished = true; return false; } @@ -245,20 +255,25 @@ } @Override + public int getStartShift() { + return mergedHighlightStartShift; + } + + @Override public int getEndOffset() { return mergedHighlightEndOffset; } @Override + public int getEndShift() { + return mergedHighlightEndShift; + } + + @Override public AttributeSet getAttributes() { return mergedAttrs; } - @Override - public boolean isStale() { - return finished; - } - void notifyLayersChanged() { // Notify that layers were changed => stop iteration finished = true; } @@ -267,59 +282,70 @@ * Do merge above the given offset. * * @param offset end of last merged highlight. + * @param shift end shift of last merged highlight (accompanying the offset). * @return top wrapper containing info about the performed merge or null * if there is zero wrappers. */ - Wrapper nextMerge(int offset) { + Wrapper nextMerge(int offset, int shift) { int i = topWrapperIndex; - for (; i >= 0 && wrappers[i].mNextChangeOffset <= offset; i--) { } - // i contains first layer which has mNextChangeOffset > offset - return updateMergeVars(i, offset); + for (; i >= 0 && wrappers[i].isMergedNextChangeBelowOrAt(offset, shift); i--) { } + // i contains first layer which has mergedNextChangeOffset > offset (or same offset but lower shift) + return updateMergeVars(i, offset, shift); } /** * Update merged vars of wrappers at (startIndex+1) and above. * - * @param startIndex index of first wrapper which has mNextChangeOffset < offset. - * All wrappers above it will have their mNextChangeOffset and mAttrs updated. - * @param offset. - * @return mNextChangeOffset of the top wrapper. + * @param startIndex index of first wrapper which has mergedNextChangeOffset above given offset (and shift) + * or -1 if all wrappers need to be updated. + * All wrappers above this index will have their mergedNextChangeOffset and mAttrs updated. + * @param offset current offset at which to update. + * @param shift current shift "within" the char at offset. + * @return top wrapper (wrapper at topWrapperIndex). */ - Wrapper updateMergeVars(int startIndex, int offset) { + Wrapper updateMergeVars(int startIndex, int offset, int shift) { Wrapper wrapper = null; int nextChangeOffset; + int nextChangeShift; AttributeSet lastAttrs; if (startIndex < 0) { // No valid layers nextChangeOffset = endOffset; + nextChangeShift = 0; lastAttrs = null; } else { wrapper = wrappers[startIndex]; - nextChangeOffset = wrapper.mNextChangeOffset; + nextChangeOffset = wrapper.mergedNextChangeOffset; + nextChangeShift = wrapper.mergedNextChangeShift; lastAttrs = wrapper.mAttrs; } - startIndex++; // Move to first wrapper that needs to be updated - for (; startIndex <= topWrapperIndex; startIndex++) { - wrapper = wrappers[startIndex]; - if (wrapper.nextChangeOffset <= offset) { - if (wrapper.updateCurrentState(offset)) { // Requires next highlight fetch - if (!wrapper.fetchNextHighlight(offset)) { // Finished all highlights in sequence - removeWrapper(startIndex); // Remove this wrapper + // Start with first wrapper that needs to be updated + wrapperIteration: + for (int i = startIndex + 1; i <= topWrapperIndex; i++) { + wrapper = wrappers[i]; + if (wrapper.isNextChangeBelowOrAt(offset, shift)) { + while (wrapper.updateCurrentState(offset, shift)) { // Check if next highlight fetch is necessary + if (!wrapper.fetchNextHighlight()) { // Finished all highlights in sequence + removeWrapper(i); // Remove this wrapper (does topWrapperIndex--) // Ensure that the wrapper returned from method is correct after removeWrapper() // topWrapperIndex already decreased by removeWrapper() - startIndex--; // Compensate for addition in for(;;) - if (startIndex == topWrapperIndex) { // Would be no more iterations - // Previous wrapper or null - wrapper = (startIndex >= 0) ? wrappers[startIndex] : null; + i--; // Compensate wrapper removal in for(;;) + if (i == topWrapperIndex) { + // Since "wrapper" variable should return wrapper at current topWrapperIndex + // that in this particular case was just removed + // then assign current top wrapper explicitly. + wrapper = (i >= 0) ? wrappers[i] : null; + break wrapperIteration; } - continue; // Use next wrapper (now at same index i) + continue wrapperIteration; } - wrapper.updateCurrentState(offset); // Update state to just fetched highlight } } - if (wrapper.nextChangeOffset < nextChangeOffset) { + if (wrapper.isNextChangeBelow(nextChangeOffset, nextChangeShift)) { nextChangeOffset = wrapper.nextChangeOffset; + nextChangeShift = wrapper.nextChangeShift; } - wrapper.mNextChangeOffset = nextChangeOffset; + wrapper.mergedNextChangeOffset = nextChangeOffset; + wrapper.mergedNextChangeShift = nextChangeShift; lastAttrs = (lastAttrs != null) ? ((wrapper.currentAttrs != null) ? AttributesUtilities.createComposite(wrapper.currentAttrs, lastAttrs) // first prior second @@ -360,14 +386,25 @@ static final class Wrapper { /** - * Layer over which hlSequence is constructed (for debugging purposes). + * Layer over which layerSequence is constructed (for debugging purposes). */ final HighlightsContainer layer; /** * Highlights sequence for layer corresponding to this wrapper. */ - final HighlightsSequence hlSequence; + final HighlightsSequence layerSequence; + + /** + * Highlights sequence supporting coloring of characters inside tabs or newlines. + */ + final ShiftHighlightsSequence shiftLayerSequence; + + /** + * End offset of the region on which upper hlSequence operates so the highlights + * returned by layerSequence should adhere to this limit too. + */ + final int endOffset; /** * Start offset of the last fetched highlight. @@ -375,23 +412,44 @@ int hlStartOffset; /** + * Possible shift accompanying hlStartOffset or zero for non-ShiftHighlightsSequence. + */ + int hlStartShift; + + /** * End offset of the last fetched highlight. */ int hlEndOffset; /** + * Possible shift accompanying hlEndOffset or zero for non-ShiftHighlightsSequence. + */ + int hlEndShift; + + /** * Attributes of the last fetched highlight. */ AttributeSet hlAttrs; /** * Offset where a change in highlighting for the current layer will occur. - * If an offset is below hlStartOffset then the value is hlStartOffset. - * Otherwise it will be hlEndOffset. + * If currently processed offset is below current highlight (fetched into hlStartOffset and hlEndOffset) + * then the value is set to hlStartOffset. + * For an offset inside current highlight the value will be set to hlEndOffset. + * Offset above hlEndOffset will trigger a next highlight fetching. */ int nextChangeOffset; /** + * If shiftHLSequence != null then (similarly to nextChangeOffset) + * this is set either to start shift of the next highlight + * or (if the offset and its shift are inside current highlight) then + * this variable is set to end shift of the current highlight + * or a next highlight will be fetched (if current offset and shift are above the highlight). + */ + int nextChangeShift; + + /** * Attributes for an offset: when before hlStartOffset it's null. * Otherwise it's hlAttrs. */ @@ -401,7 +459,12 @@ * Merged next change offset: minimum of nextChangeOffset from all * wrappers below this one in the wrappers array. */ - int mNextChangeOffset; + int mergedNextChangeOffset; + + /** + * Merged next change shift - possible shift accompanying mergedNextChangeOffset or zero. + */ + int mergedNextChangeShift; /** * Merged attributes: merge of currentAttrs from all @@ -412,34 +475,98 @@ private int emptyHighlightCount; - public Wrapper(HighlightsContainer layer, HighlightsSequence hlSequence, int startOffset) { + public Wrapper(HighlightsContainer layer, HighlightsSequence hlSequence, int endOffset) { this.layer = layer; - this.hlSequence = hlSequence; - fetchNextHighlight(startOffset); - updateCurrentState(startOffset); - this.mNextChangeOffset = startOffset; // Will cause recomputation + this.layerSequence = hlSequence; + this.shiftLayerSequence = (hlSequence instanceof ShiftHighlightsSequence) ? (ShiftHighlightsSequence) hlSequence : null; + this.endOffset = endOffset; } - boolean isFinished() { // Whether no more highlights from - return (hlStartOffset == Integer.MAX_VALUE); + boolean init(int startOffset) { + do { + if (!fetchNextHighlight()) { + return false; + } + } while (hlEndOffset <= startOffset); // Exclude any possible highlights ending below startOffset + updateCurrentState(startOffset, 0); + return true; } /** + * Whether next change offset and shift of this wrapper are below the given parameters. + * + * @param offset current offset. + * @param shift current shift (accompanying the current offset). + * @return true if next change offset and shift of this wrapper are below the given parameters + * or false otherwise. + */ + boolean isNextChangeBelow(int offset, int shift) { + return nextChangeOffset < offset || (nextChangeOffset == offset && nextChangeShift < shift); + } + + /** + * Whether next change offset and shift of this wrapper are below the given parameters + * or right at them. + * + * @param offset current offset. + * @param shift current shift (accompanying the current offset). + * @return true if next change offset and shift of this wrapper are below or right at the given parameters + * or false otherwise. + */ + boolean isNextChangeBelowOrAt(int offset, int shift) { + return nextChangeOffset < offset || (nextChangeOffset == offset && nextChangeShift <= shift); + } + + /** + * Whether merged next change offset and shift of this wrapper are below the given parameters + * or right at them. + * + * @param offset current offset. + * @param shift current shift (accompanying the current offset). + * @return true if next change offset and shift of this wrapper are below or right at the given parameters + * or false otherwise. + */ + boolean isMergedNextChangeBelowOrAt(int offset, int shift) { + return mergedNextChangeOffset < offset || (mergedNextChangeOffset == offset && mergedNextChangeShift <= shift); + } + + /** * Update currentAttrs and nextChangeOffset according to given offset. * @param offset offset to which to update + * @param shift shift inside tab or newline character on the given offset. * @return true if the offset is >= hlEndOffset and so fetchNextHighlight() is necessary. */ - boolean updateCurrentState(int offset) { - if (offset < hlStartOffset) { // before hl start + boolean updateCurrentState(int offset, int shift) { + if (offset < hlStartOffset) { // offset before current hl start currentAttrs = null; nextChangeOffset = hlStartOffset; + nextChangeShift = hlStartShift; return false; - } else if (offset < hlEndOffset) { // inside hl (assuming call after fetchNextHighlight()) + } else if (offset == hlStartOffset) { // inside hl (assuming call after fetchNextHighlight()) + if (shift < hlStartShift) { + currentAttrs = null; + nextChangeOffset = hlStartOffset; + nextChangeShift = hlStartShift; + return false; + + } else { // Above (or at) highlight's start + if (offset < hlEndOffset || (offset == hlEndOffset && shift < hlEndShift)) { + currentAttrs = hlAttrs; + nextChangeOffset = hlEndOffset; + nextChangeShift = hlEndShift; + return false; + } else { + return true; // Fetch next highlight + } + } // else: fetch next highlight + } else if (offset < hlEndOffset || (offset == hlEndOffset && shift < hlEndShift)) { currentAttrs = hlAttrs; nextChangeOffset = hlEndOffset; + nextChangeShift = hlEndShift; return false; - } // else: offset >= hlEndOffset - return true; + } else { // Above hlEndOffset (or hlEndShift) => fetch next highlight + return true; // Fetch next highlight + } } /** @@ -447,56 +574,62 @@ * @param offset * @return true if highlight fetched successfully or false if there are no more highlights. */ - boolean fetchNextHighlight(int offset) { - assert (hlStartOffset != Integer.MAX_VALUE); - do { - if (hlSequence.moveNext()) { - hlStartOffset = hlSequence.getStartOffset(); - if (hlStartOffset < hlEndOffset) { // Invalid layer: next highlight overlaps previous one + boolean fetchNextHighlight() { + while (layerSequence.moveNext()) { // Loop to allow for skip over empty highlights + hlStartOffset = layerSequence.getStartOffset(); + if (hlStartOffset < hlEndOffset) { // Invalid layer: next highlight overlaps previous one + // To prevent infinite loops finish this HL + if (LOG.isLoggable(Level.FINE)) { + LOG.fine("Disabled an invalid highlighting layer: hlStartOffset=" + hlStartOffset + // NOI18N + " < previous hlEndOffset=" + hlEndOffset + " for layer=" + layer); // NOI18N + } + return false; + } + hlEndOffset = layerSequence.getEndOffset(); + if (hlEndOffset <= hlStartOffset) { + if (hlEndOffset < hlStartOffset) { // Invalid highlight: end offset before start offset // To prevent infinite loops finish this HL if (LOG.isLoggable(Level.FINE)) { LOG.fine("Disabled an invalid highlighting layer: hlStartOffset=" + hlStartOffset + // NOI18N - " < previous hlEndOffset=" + hlEndOffset + " for layer=" + layer); // NOI18N + " > hlEndOffset=" + hlEndOffset + " for layer=" + layer); // NOI18N } - hlStartOffset = hlEndOffset = Integer.MAX_VALUE; return false; } - hlEndOffset = hlSequence.getEndOffset(); - if (hlEndOffset <= hlStartOffset) { - if (hlEndOffset < hlStartOffset) { // Invalid highlight: end offset before start offset - // To prevent infinite loops finish this HL - if (LOG.isLoggable(Level.FINE)) { - LOG.fine("Disabled an invalid highlighting layer: hlStartOffset=" + hlStartOffset + // NOI18N - " > hlEndOffset=" + hlEndOffset + " for layer=" + layer); // NOI18N - } - hlStartOffset = hlEndOffset = Integer.MAX_VALUE; - return false; + emptyHighlightCount++; + if (emptyHighlightCount >= MAX_EMPTY_HIGHLIGHT_COUNT) { + if (LOG.isLoggable(Level.FINE)) { + LOG.fine("Disabled an invalid highlighting layer: too many empty highlights=" + emptyHighlightCount); // NOI18N } - emptyHighlightCount++; - if (emptyHighlightCount >= MAX_EMPTY_HIGHLIGHT_COUNT) { - if (LOG.isLoggable(Level.FINE)) { - LOG.fine("Disabled an invalid highlighting layer: too many empty highlights=" + emptyHighlightCount); // NOI18N - } - hlStartOffset = hlEndOffset = Integer.MAX_VALUE; - return false; - } + return false; } - hlAttrs = hlSequence.getAttributes(); - if (LOG.isLoggable(Level.FINER)) { - LOG.fine("Fetched highlight: <" + hlStartOffset + // NOI18N - "," + hlEndOffset + "> for layer=" + layer + '\n'); // NOI18N + continue; // Fetch next highlight + } + if (hlEndOffset > endOffset) { + if (hlStartOffset >= endOffset) { + return false; } - } else { - hlStartOffset = hlEndOffset = Integer.MAX_VALUE; // Signal that sequence is finished - return false; + hlEndOffset = endOffset; // Limit the highlight to endOffset - it should still remain non-empty } - } while (hlEndOffset <= offset); - return true; // Valid highlight fetched + if (shiftLayerSequence != null) { + hlStartShift = shiftLayerSequence.getStartShift(); + hlEndShift = shiftLayerSequence.getEndShift(); + // Do not perform extra checking of validity (non-overlapping with previous highlight + // and validity of shifts since it should not be crucial + // for proper functioning of updateCurrentState() method. + } // else hlStartShift and hlEndShift are always zero in the wrapper + hlAttrs = layerSequence.getAttributes(); + if (LOG.isLoggable(Level.FINER)) { + LOG.fine("Fetched highlight: <" + hlStartOffset + // NOI18N + "," + hlEndOffset + "> for layer=" + layer + '\n'); // NOI18N + } + return true; // Valid highlight fetched + } + return false; // No more highlights } @Override public String toString() { - return "M[" + mNextChangeOffset + ",A=" + mAttrs + // NOI18N + return "M[" + mergedNextChangeOffset + ",A=" + mAttrs + // NOI18N "] Next[" + nextChangeOffset + ",A=" + currentAttrs + // NOI18N "] HL:<" + hlStartOffset + "," + hlEndOffset + ">,A=" + hlAttrs; // NOI18N } diff --git a/editor.lib2/src/org/netbeans/modules/editor/lib2/highlighting/Factory.java b/editor.lib2/src/org/netbeans/modules/editor/lib2/highlighting/Factory.java --- a/editor.lib2/src/org/netbeans/modules/editor/lib2/highlighting/Factory.java +++ b/editor.lib2/src/org/netbeans/modules/editor/lib2/highlighting/Factory.java @@ -114,6 +114,13 @@ true, // fixed size new WhitespaceHighlighting(context.getComponent())) ); + + layers.add(HighlightsLayer.create( + CaretOverwriteModeHighlighting.LAYER_TYPE_ID, + ZOrder.TOP_RACK.forPosition(100), + true, // fixed size + new CaretOverwriteModeHighlighting(context.getComponent())) + ); return layers.toArray(new HighlightsLayer [layers.size()]); } diff --git a/editor.lib2/src/org/netbeans/modules/editor/lib2/highlighting/HighlightingManager.java b/editor.lib2/src/org/netbeans/modules/editor/lib2/highlighting/HighlightingManager.java --- a/editor.lib2/src/org/netbeans/modules/editor/lib2/highlighting/HighlightingManager.java +++ b/editor.lib2/src/org/netbeans/modules/editor/lib2/highlighting/HighlightingManager.java @@ -81,10 +81,10 @@ /** * Highlighting manager maintains all the highlighting layers instances. - *
    + *
    * It divides them into two groups according to their z-order and fixedSize attributes. * Top layers that have fixedSize set to true are one group. The rest is the other group. - *
    + *
    * View hierarchy only rebuilds views if the second group of layers (bottom ones) changes. * If the top group changes the view hierarchy only triggers repaint of affected part. * diff --git a/editor.lib2/src/org/netbeans/modules/editor/lib2/highlighting/HighlightsList.java b/editor.lib2/src/org/netbeans/modules/editor/lib2/highlighting/HighlightsList.java --- a/editor.lib2/src/org/netbeans/modules/editor/lib2/highlighting/HighlightsList.java +++ b/editor.lib2/src/org/netbeans/modules/editor/lib2/highlighting/HighlightsList.java @@ -56,7 +56,7 @@ /** * List of highlight items. First highlights item starts at startOffset. - *
    + *
    * GapList is used due to its copyElements() method. */ private HighlightItem[] highlightItems; @@ -128,7 +128,7 @@ * Create attribute set covering {@link #startOffset()} till maxEndOffset * or lower offset if font would differ for the particular attribute set * of an item. - *
    + *
    * The list must cover cutEndOffset otherwise the behavior is undefined. * * @param defaultFont default font to which the attributes in highlight items @@ -251,7 +251,7 @@ /** * Create attribute set covering single character at {@link #startOffset()}. - *
    + *
    * The list must cover {@link #startOffset()} otherwise the behavior is undefined. * * @return attribute set. diff --git a/editor.lib2/src/org/netbeans/modules/editor/lib2/highlighting/ReadOnlyFilesHighlighting.java b/editor.lib2/src/org/netbeans/modules/editor/lib2/highlighting/ReadOnlyFilesHighlighting.java --- a/editor.lib2/src/org/netbeans/modules/editor/lib2/highlighting/ReadOnlyFilesHighlighting.java +++ b/editor.lib2/src/org/netbeans/modules/editor/lib2/highlighting/ReadOnlyFilesHighlighting.java @@ -114,7 +114,7 @@ if (LOG.isLoggable(Level.FINE)) { LOG.fine("Highlighting file " + file + " in <" + startOffset + ", " + endOffset + ">"); //NOI18N } - return new CaretBasedBlockHighlighting.SimpleHighlightsSequence( + return new SimpleHighlightsSequence( Math.max(0, startOffset), Math.min(document.getLength(), endOffset), attribs); @@ -215,4 +215,44 @@ } return null; } + + private static final class SimpleHighlightsSequence implements HighlightsSequence { + + private int startOffset; + private int endOffset; + private AttributeSet attribs; + + private boolean end = false; + + public SimpleHighlightsSequence(int startOffset, int endOffset, AttributeSet attribs) { + this.startOffset = startOffset; + this.endOffset = endOffset; + this.attribs = attribs; + } + + @Override + public boolean moveNext() { + if (!end) { + end = true; + return true; + } else { + return false; + } + } + + @Override + public int getStartOffset() { + return startOffset; + } + + @Override + public int getEndOffset() { + return endOffset; + } + + @Override + public AttributeSet getAttributes() { + return attribs; + } + } // End of SimpleHighlightsSequence } diff --git a/editor.lib2/src/org/netbeans/modules/editor/lib2/highlighting/SyntaxHighlighting.java b/editor.lib2/src/org/netbeans/modules/editor/lib2/highlighting/SyntaxHighlighting.java --- a/editor.lib2/src/org/netbeans/modules/editor/lib2/highlighting/SyntaxHighlighting.java +++ b/editor.lib2/src/org/netbeans/modules/editor/lib2/highlighting/SyntaxHighlighting.java @@ -79,7 +79,7 @@ /** * The syntax coloring layer. - *
    + *
    * It excludes newline chars from any colorings so that if e.g. a whitespace highlighting is set * the rest of line after newline is not colored. * diff --git a/editor.lib2/src/org/netbeans/modules/editor/lib2/highlighting/VisualMark.java b/editor.lib2/src/org/netbeans/modules/editor/lib2/highlighting/VisualMark.java --- a/editor.lib2/src/org/netbeans/modules/editor/lib2/highlighting/VisualMark.java +++ b/editor.lib2/src/org/netbeans/modules/editor/lib2/highlighting/VisualMark.java @@ -44,7 +44,7 @@ /** * Visual mark encapsulates y-coordinate offset together with an object * that provides an offset (assumed that it's tracked as a SWing position). - *
    + *
    * The y-coordinate is tracked as a raw value that must first be preprocessed * by {@link VisualMarkVector}. * @@ -62,7 +62,7 @@ /** * Get offset of this visual mark. - *
    + *
    * It's assumed that the offset is tracked as a Swing position. * * @return >=0 offset of this mark. diff --git a/editor.lib2/src/org/netbeans/modules/editor/lib2/highlighting/WhitespaceHighlighting.java b/editor.lib2/src/org/netbeans/modules/editor/lib2/highlighting/WhitespaceHighlighting.java --- a/editor.lib2/src/org/netbeans/modules/editor/lib2/highlighting/WhitespaceHighlighting.java +++ b/editor.lib2/src/org/netbeans/modules/editor/lib2/highlighting/WhitespaceHighlighting.java @@ -60,7 +60,7 @@ /** * Highlighting of indentation and trailing whitespace. - *
    + *
    * If there's no non-WS text on the line the whitespace is treated as trailing. * * @author Miloslav Metelka diff --git a/editor.lib2/src/org/netbeans/modules/editor/lib2/search/TextStorageSet.java b/editor.lib2/src/org/netbeans/modules/editor/lib2/search/TextStorageSet.java --- a/editor.lib2/src/org/netbeans/modules/editor/lib2/search/TextStorageSet.java +++ b/editor.lib2/src/org/netbeans/modules/editor/lib2/search/TextStorageSet.java @@ -47,7 +47,7 @@ /** * Set of character sequences allowing to compare a part of character sequence. - *
    + *
    * Note: Character sequences stored must be comparable by their equals() method. * * @author Miloslav Metelka diff --git a/editor.lib2/src/org/netbeans/modules/editor/lib2/view/AttributedCharSequence.java b/editor.lib2/src/org/netbeans/modules/editor/lib2/view/AttributedCharSequence.java --- a/editor.lib2/src/org/netbeans/modules/editor/lib2/view/AttributedCharSequence.java +++ b/editor.lib2/src/org/netbeans/modules/editor/lib2/view/AttributedCharSequence.java @@ -49,10 +49,10 @@ /** * Char sequence with additional attributes. - *
    + *
    * Method operate with javax.swing.text.AttributeSet which they transfer * to corresponding TextAttribute constants. - *
    + *
    * After creation the series of addTextRun() is called followed by setText() * which completes modification and makes the object ready for clients. * diff --git a/editor.lib2/src/org/netbeans/modules/editor/lib2/view/DocumentView.java b/editor.lib2/src/org/netbeans/modules/editor/lib2/view/DocumentView.java --- a/editor.lib2/src/org/netbeans/modules/editor/lib2/view/DocumentView.java +++ b/editor.lib2/src/org/netbeans/modules/editor/lib2/view/DocumentView.java @@ -72,7 +72,7 @@ /** * View representing the whole document. - *
    + *
    * It consists of individual paragraph views that typically map to line elements * but e.g. code folding may cause multiple line elements correspond to one line view. * @@ -165,7 +165,7 @@ /** * Start offset of the document view after last processed modification. - *
    + *
    * This is used during updating of the document view by document modification. * For undo of removal the position of first paragraph may restore inside the inserted area * and holding integer offset allows to know the original start offset. @@ -174,7 +174,7 @@ /** * Start offset of the document view. - *
    + *
    * This is used during updating of the document view by document modification. * For undo of removal the position of last paragraph may restore inside the inserted area * and holding integer offset allows to know the original end offset of the document view. @@ -472,7 +472,7 @@ /** * Get current allocation of the document view by using size from last call * to {@link #setSize(float, float)}. - *
    + *
    * Returned instance may not be mutated (use getAllocationMutable()). * * @return current allocation of document view. diff --git a/editor.lib2/src/org/netbeans/modules/editor/lib2/view/DocumentViewChildren.java b/editor.lib2/src/org/netbeans/modules/editor/lib2/view/DocumentViewChildren.java --- a/editor.lib2/src/org/netbeans/modules/editor/lib2/view/DocumentViewChildren.java +++ b/editor.lib2/src/org/netbeans/modules/editor/lib2/view/DocumentViewChildren.java @@ -63,11 +63,11 @@ /** * Class that manages children of {@link EditorBoxView}. - *
    + *
    * The class can manage offsets of children in case {@link #rawEndOffsetManaged()} * returns true. In such case each child must properly implement {@link EditorView#getRawEndOffset()} * and the maintained raw offsets are relative to corresponding box view's getStartOffset(). - *
    + *
    * Generally children of {@link #ParagraphView} manage their raw end offsets * while children of {@link #DocumentView} do not manage them (they use Position objects * to manage its start). @@ -125,7 +125,7 @@ /** * Replace paragraph views inside DocumentView. - *
    + *
    * In case both removeCount == 0 and addedViews is empty this method does not need to be called. * * @param docView @@ -242,7 +242,7 @@ /** * Get view index of first view that "contains" the offset (starts with it or it's inside) * by examining child views' absolute start offsets. - *
    + *
    * This is suitable for document view where start offsets of paragraph views * are maintained as positions. * diff --git a/editor.lib2/src/org/netbeans/modules/editor/lib2/view/DocumentViewOp.java b/editor.lib2/src/org/netbeans/modules/editor/lib2/view/DocumentViewOp.java --- a/editor.lib2/src/org/netbeans/modules/editor/lib2/view/DocumentViewOp.java +++ b/editor.lib2/src/org/netbeans/modules/editor/lib2/view/DocumentViewOp.java @@ -197,7 +197,7 @@ /** * Whether the "children" is currently reflecting the document state. - *
    + *
    * The children may hold a semi-valid view hierarchy which may still be partly used * to resolve queries in some cases. */ @@ -322,7 +322,7 @@ * Constant retrieved from preferences settings that allows the user to increase/decrease * paragraph view's height (which may help for problematic fonts that do not report its height * correctly for them to fit the line). - *
    + *
    * By default it's 1.0. All the ascents, descent and leadings of all fonts * are multiplied by the constant. */ @@ -1221,7 +1221,7 @@ /** * Get displayed portion of the component (either viewport.getViewRect()) * or (if viewport is missing) size of the component. - *
    + *
    * Note: The value may be obsolete during paint - clipping bounds may already * incorporate a just performed scroll while visibleRect does not yet. * diff --git a/editor.lib2/src/org/netbeans/modules/editor/lib2/view/EditorTabExpander.java b/editor.lib2/src/org/netbeans/modules/editor/lib2/view/EditorTabExpander.java --- a/editor.lib2/src/org/netbeans/modules/editor/lib2/view/EditorTabExpander.java +++ b/editor.lib2/src/org/netbeans/modules/editor/lib2/view/EditorTabExpander.java @@ -51,7 +51,7 @@ /** * Tab expander of the editor. - *
    + *
    * It aligns visually (not by number of characters) since it's a desired behavior * for asian languages characters that visually occupy two regular characters and they should * be treated in that way in terms of tab aligning. diff --git a/editor.lib2/src/org/netbeans/modules/editor/lib2/view/EditorView.java b/editor.lib2/src/org/netbeans/modules/editor/lib2/view/EditorView.java --- a/editor.lib2/src/org/netbeans/modules/editor/lib2/view/EditorView.java +++ b/editor.lib2/src/org/netbeans/modules/editor/lib2/view/EditorView.java @@ -60,16 +60,16 @@ /** * Base class for views in editor view hierarchy. - *
    + *
    * In general there are three types of views:
      *
    • Document view
    • *
    • Paragraph views
    • *
    • Children of paragraph views which include highlights view, newline view and others.
    • *
    - *
    + *
    * Paragraph views have their start offset based over a swing text position. Their end offset * is based on last child's end offset. - *
    + *
    * Children of paragraph views have their start offset based over a relative distance * to their parent's paragraph view's start offset. Therefore their start offset does not mutate * upon modification unless the whole paragraph's start offset mutates. @@ -97,7 +97,7 @@ /** * Get raw end offset of the view which may transform to real end offset * when post-processed by parent view. - *
    + *
    * Note: Typical clients should NOT call this method (they should call * {@link #getEndOffset()} method instead). * diff --git a/editor.lib2/src/org/netbeans/modules/editor/lib2/view/EditorViewFactory.java b/editor.lib2/src/org/netbeans/modules/editor/lib2/view/EditorViewFactory.java --- a/editor.lib2/src/org/netbeans/modules/editor/lib2/view/EditorViewFactory.java +++ b/editor.lib2/src/org/netbeans/modules/editor/lib2/view/EditorViewFactory.java @@ -55,14 +55,14 @@ /** * SPI class allowing to produce views. - *
    + *
    * There are two main factories: for folds and for highlights (the default one). * The factories have a priority and factory with highest priority * will always "win" in terms that its view will surely be created as desired. * Factories at lower levels will receive a limitOffset into createView() * being a start offset of the view produced by a higher level factory. * The factory may decide whether it will create view with limiting offset or not. - *
    + *
    * Factory generally operates in two modes:
      *
    • Regular mode when the factory produces views
    • *
    • Offset mode when the factory only returns bounds of the produced views @@ -152,7 +152,7 @@ /** * Notify next offset area where views will be created in case endCreationOffset * in {@link #restart(int, int)} was exceeded. - *
      + *
      * This method may be called multiple times if views building still does not match * original views boundaries. * @@ -165,7 +165,7 @@ /** * Return starting offset of the next view to be produced by this view factory. - *
      + *
      * This method gets called after restarting of this view factory * (with a startOffset parameter passed to {@link #restart(int)}) * and also after any of the registered view factories created a view @@ -181,7 +181,7 @@ * Create a view at the given offset. The view factory must determine * the appropriate end offset of the produced view and set its length * returned by {@link EditorView#getLength()} appropriately. - *
      + *
      * This method is only called if the factory is in view-producing mode * (its {@link #viewEndOffset(startOffset, limitOffset)} is not called). * @@ -197,10 +197,10 @@ * @param origView original view located at the given position (it may have a different * physical offset due to just performed modification but it corresponds to the same text * in the document). It may be null if there is no view to reuse. - *
      + *
      * The factory may not return the given instance but it may reuse an arbitrary information * from it. - *
      + *
      * For example for text layout reuse the highlights view factory will first check if the view * is non-null and matches views produced by it then it will check * if the new view has same length as the original one and that the view attributes @@ -222,7 +222,7 @@ /** * Return to-be-created view's end offset. - *
      + *
      * This method is only called when createViews parameter * in {@link #restart(int, int, boolean)} is false. In such mode no physical views * are created and only view boundaries of potential views are being determined. @@ -240,7 +240,7 @@ /** * Finish this round of views creation. - *
      + *
      * {@link #restart(int) } may be called subsequently to init a new round * of views creation. */ @@ -263,7 +263,7 @@ /** * Schedule repaint request on the view hierarchy. - *
      + *
      * Document must be read-locked prior calling this method. * * @param startOffset @@ -277,12 +277,12 @@ * Signal that this view factory is no longer able to produce * valid views due to some serious changes that it processes * (for example highlights change for HighlightsViewFactory). - *
      + *
      * View creation may be stopped immediately by the caller and restarted to get * the correct views. However if it would fail periodically the caller may decide * to continue the creation to have at least some views. In both cases * the view factory should be able to continue working normally. - *
      + *
      * This method can be called from any thread. */ protected final void notifyStaleCreation() { @@ -303,7 +303,7 @@ /** * Notification that this factory is no longer being used so it should * release its resources - for example detach all listeners. - *
      + *
      * It's called upon document view receives setParent(null) which typically signals * that a new document view will be created for the particular editor pane. */ diff --git a/editor.lib2/src/org/netbeans/modules/editor/lib2/view/EditorViewFactoryChange.java b/editor.lib2/src/org/netbeans/modules/editor/lib2/view/EditorViewFactoryChange.java --- a/editor.lib2/src/org/netbeans/modules/editor/lib2/view/EditorViewFactoryChange.java +++ b/editor.lib2/src/org/netbeans/modules/editor/lib2/view/EditorViewFactoryChange.java @@ -43,7 +43,7 @@ /** * Offset range describing change in particular view factory. - *
      + *
      * Each factory may fire a list of changes of different types at once. * * @author Miloslav Metelka diff --git a/editor.lib2/src/org/netbeans/modules/editor/lib2/view/HighlightsView.java b/editor.lib2/src/org/netbeans/modules/editor/lib2/view/HighlightsView.java --- a/editor.lib2/src/org/netbeans/modules/editor/lib2/view/HighlightsView.java +++ b/editor.lib2/src/org/netbeans/modules/editor/lib2/view/HighlightsView.java @@ -60,7 +60,7 @@ /** * View with highlights. This is the most used view. - *
      + *
      * It can either have no highlights (attributes == null) or simple attributes * (attributes == non- * diff --git a/editor.lib2/src/org/netbeans/modules/editor/lib2/view/HighlightsViewFactory.java b/editor.lib2/src/org/netbeans/modules/editor/lib2/view/HighlightsViewFactory.java --- a/editor.lib2/src/org/netbeans/modules/editor/lib2/view/HighlightsViewFactory.java +++ b/editor.lib2/src/org/netbeans/modules/editor/lib2/view/HighlightsViewFactory.java @@ -67,7 +67,7 @@ /** * View factory returning highlights views. It is specific in that it always * covers the whole document area by views even if there are no particular highlights - *
      + *
      * Currently the factory coalesces highlights change requests from non-AWT thread. * * @author Miloslav Metelka @@ -94,7 +94,7 @@ * then the infrastructure will attempt to end current long view creation * at a given nextOrigViewOffset parameter in order to save views creation and reuse * existing text layouts (and their slit text layouts for line wrapping). - *
      + *
      * The user would have to insert or remove LONG_VIEW_TOLERANCE of characters into long view * in order to force the factory to not match to the given nextOrigViewOffset. */ diff --git a/editor.lib2/src/org/netbeans/modules/editor/lib2/view/HighlightsViewUtils.java b/editor.lib2/src/org/netbeans/modules/editor/lib2/view/HighlightsViewUtils.java --- a/editor.lib2/src/org/netbeans/modules/editor/lib2/view/HighlightsViewUtils.java +++ b/editor.lib2/src/org/netbeans/modules/editor/lib2/view/HighlightsViewUtils.java @@ -66,6 +66,8 @@ import javax.swing.text.Position.Bias; import javax.swing.text.StyleConstants; import javax.swing.text.View; +import org.netbeans.api.editor.caret.CaretInfo; +import org.netbeans.api.editor.caret.EditorCaret; import org.netbeans.api.editor.settings.EditorStyleConstants; import org.netbeans.lib.editor.util.CharSequenceUtilities; import org.netbeans.lib.editor.util.swing.DocumentUtilities; @@ -73,7 +75,7 @@ /** * Utilities related to HighlightsView and TextLayout management. - *
      + *
      * Unfortunately the TextLayout based on AttributedCharacterIterator does not handle * correctly italic fonts (at least on Mac it renders background rectangle non-italicized). * Therefore child views with foreground that differs from text layout's "global" foreground @@ -159,7 +161,16 @@ return 0d; } Caret caret = textComponent.getCaret(); - Point magicCaretPoint = (caret != null) ? caret.getMagicCaretPosition() : null; + Point magicCaretPoint = null; + if(caret != null) { + if(caret instanceof EditorCaret) { + EditorCaret editorCaret = (EditorCaret) caret; + CaretInfo info = editorCaret.getCaretAt(offset); + magicCaretPoint = (info != null) ? info.getMagicCaretPosition() : null; + } else { + magicCaretPoint = caret.getMagicCaretPosition(); + } + } double x; if (magicCaretPoint == null) { Shape offsetBounds = view.modelToViewChecked(offset, alloc, bias); @@ -528,7 +539,7 @@ /** * Paint strike-through line for a font currently set to the graphics * with the color currently set to the graphics. - *
      + *
      * It's assumed that the clipping is set appropriately because the method * renders whole textLayoutAlloc with the strike-through. * diff --git a/editor.lib2/src/org/netbeans/modules/editor/lib2/view/LockedViewHierarchy.java b/editor.lib2/src/org/netbeans/modules/editor/lib2/view/LockedViewHierarchy.java --- a/editor.lib2/src/org/netbeans/modules/editor/lib2/view/LockedViewHierarchy.java +++ b/editor.lib2/src/org/netbeans/modules/editor/lib2/view/LockedViewHierarchy.java @@ -44,10 +44,10 @@ /** * Locked view hierarchy as result of {@link ViewHierarchy#lock() }. - *
      + *
      * Underlying document of the view hierarchy's text component must be * read-locked to guarantee stability of offsets passed to methods of this class. - *
      + *
      * If editor view hierarchy is not installed into text component * (text component's root view is not an instance of DocumentView) * the methods return default values as described in their documentation. @@ -82,7 +82,7 @@ /** * Get text component that this view hierarchy is associated with. - *
      + *
      * * @return non-null text component. */ @@ -93,10 +93,10 @@ /** * Get y coordinate of a visual row that corresponds to given offset. - *
      + *
      * Underlying document of the view hierarchy's text component should be read-locked * to guarantee stability of passed offset. - *
      + *
      * If editor view hierarchy is not installed into text component this method * delegates to {@link JTextComponent#modelToView(int) }. * @@ -197,9 +197,9 @@ /** * Get height of a visual row of text. - *
      + *
      * For wrapped lines (containing multiple visual rows) this is height of a single visual row. - *
      + *
      * Current editor view hierarchy implementation uses uniform row height for all the rows. * * @return height of a visual row. @@ -211,7 +211,7 @@ /** * Get width of a typical character of a default font used by view hierarchy. - *
      + *
      * In case mixed fonts (non-monospaced) are used this gives a little value * but certain tools such as rectangular selection may use this value. */ @@ -222,10 +222,10 @@ /** * Return true if the view hierarchy is actively managing its contained views. - *
      + *
      * Infrastructure may turn the view hierarchy inactive in case there are many * edits performed in the document (mainly during code reformatting). - *
      + *
      * Also the view hierarchy is not active when a document modification was performed * but the view hierarchy did not update itself accordingly yet (its DocumentListener * was not called yet). diff --git a/editor.lib2/src/org/netbeans/modules/editor/lib2/view/OffsetRegion.java b/editor.lib2/src/org/netbeans/modules/editor/lib2/view/OffsetRegion.java --- a/editor.lib2/src/org/netbeans/modules/editor/lib2/view/OffsetRegion.java +++ b/editor.lib2/src/org/netbeans/modules/editor/lib2/view/OffsetRegion.java @@ -127,7 +127,7 @@ /** * Return union of this region with the given region. - *
      + *
      * If the given region is empty then return "this" region. * * @param doc non-null document to which the offsets relate. @@ -164,7 +164,7 @@ /** * Return union of this region with the given region. - *
      + *
      * If the given region is empty then return "this" region. * * @param region region to union with. diff --git a/editor.lib2/src/org/netbeans/modules/editor/lib2/view/ParagraphView.java b/editor.lib2/src/org/netbeans/modules/editor/lib2/view/ParagraphView.java --- a/editor.lib2/src/org/netbeans/modules/editor/lib2/view/ParagraphView.java +++ b/editor.lib2/src/org/netbeans/modules/editor/lib2/view/ParagraphView.java @@ -66,9 +66,9 @@ /** * View of a line typically spans a single textual line of a corresponding document * but it may span several lines if it contains a fold view for collapsed code fold. - *
      + *
      * It is capable to do a word-wrapping. - *
      + *
      * It is not tight to any element (its element is null). * Its contained views may span multiple lines (e.g. in case of code folding). * @@ -82,15 +82,15 @@ /** * Whether children are non-null and contain up-to-date views. - *
      + *
      * If false then either children == null (children not computed yet) * or some (or all) child views are marked as invalid (they should be recomputed * since some highlight factories reported particular text span as changed). - *
      + *
      * The local offset range of invalid children can be obtained (if children != null) * by children.getStartInvalidChildrenLocalOffset() * and children.getEndInvalidChildrenLocalOffset()). - *
      + *
      * When all children are dropped (children == null) then LAYOUT_VALID is cleared too. */ private static final int CHILDREN_VALID = 1; @@ -98,11 +98,11 @@ /** * Whether layout information is valid for this paragraph view * (note that layout may be valid even when some children were marked as invalid). - *
      + *
      * Since span of child views is initialized upon views replace * the layout updating means checking whether the pView is too wide * and thus needs to compute wrap lines and building of those wrap lines. - *
      + *
      * Whether particular operation (mainly model-to-view, view-to-model and painting operations) * needs an up-to-date layout is upon decision of each operation * (done in DocumentViewChildren). @@ -255,7 +255,7 @@ /** * Check whether layout must be updated. - *
      + *
      * It should only be called when children != null. * * @return true if layout update was necessary or false otherwise. diff --git a/editor.lib2/src/org/netbeans/modules/editor/lib2/view/ParagraphViewChildren.java b/editor.lib2/src/org/netbeans/modules/editor/lib2/view/ParagraphViewChildren.java --- a/editor.lib2/src/org/netbeans/modules/editor/lib2/view/ParagraphViewChildren.java +++ b/editor.lib2/src/org/netbeans/modules/editor/lib2/view/ParagraphViewChildren.java @@ -373,7 +373,7 @@ /** * Layout pView's children according to line wrap setting. - *
      + *
      * This method should ONLY be called by pView which then re-checks children size * and possibly updates itself appropriately. */ diff --git a/editor.lib2/src/org/netbeans/modules/editor/lib2/view/ParagraphViewDescriptor.java b/editor.lib2/src/org/netbeans/modules/editor/lib2/view/ParagraphViewDescriptor.java --- a/editor.lib2/src/org/netbeans/modules/editor/lib2/view/ParagraphViewDescriptor.java +++ b/editor.lib2/src/org/netbeans/modules/editor/lib2/view/ParagraphViewDescriptor.java @@ -58,7 +58,7 @@ /** * Get start offset of the paragraph view represented by this descriptor. - *
      + *
      * When a LockedViewHierarchy that provided this paragraph view descriptor * is unlocked then operation of this method is undefined. * @@ -70,7 +70,7 @@ /** * Get textual length of the paragraph view represented by this descriptor. - *
      + *
      * When a LockedViewHierarchy that provided this paragraph view descriptor * is unlocked then operation of this method is undefined. * @@ -82,7 +82,7 @@ /** * Get visual allocation of the whole paragraph view (represented by this descriptor). - *
      + *
      * When a LockedViewHierarchy that provided this paragraph view descriptor * is unlocked then operation of this method is undefined. * @@ -95,11 +95,11 @@ /** * Get ascent (useful for text rendering using a particular font) * of the paragraph view represented by this descriptor. - *
      + *
      * This method is useful when a tool (such as a side bar performing rendering of a line * number) wants to render a text that should vertically match the text * rendered by the paragraph view. - *
      + *
      * When a LockedViewHierarchy that provided this paragraph view descriptor * is unlocked then operation of this method is undefined. * diff --git a/editor.lib2/src/org/netbeans/modules/editor/lib2/view/TabView.java b/editor.lib2/src/org/netbeans/modules/editor/lib2/view/TabView.java --- a/editor.lib2/src/org/netbeans/modules/editor/lib2/view/TabView.java +++ b/editor.lib2/src/org/netbeans/modules/editor/lib2/view/TabView.java @@ -58,13 +58,13 @@ /** * View of (possibly multiple) '\t' characters. - *
      + *
      * It needs to be measured specially - it needs to get visually aligned to multiples * of TAB_SIZE char width. - *
      + *
      * Note that the view does not hold a last tab expander itself by itself so if tab expander * changes the view does not call a preference change. - *
      + *
      * Due to line wrap the view cannot base its tab-stop calculations upon alloc.getX(). * * diff --git a/editor.lib2/src/org/netbeans/modules/editor/lib2/view/TextLayoutBreakInfo.java b/editor.lib2/src/org/netbeans/modules/editor/lib2/view/TextLayoutBreakInfo.java --- a/editor.lib2/src/org/netbeans/modules/editor/lib2/view/TextLayoutBreakInfo.java +++ b/editor.lib2/src/org/netbeans/modules/editor/lib2/view/TextLayoutBreakInfo.java @@ -45,7 +45,7 @@ /** * Info about last successful breaking of the view. - *
      + *
      * Since TextLayout creation is expensive this should speed up breaking of the views * (used in line wrap) considerably. * diff --git a/editor.lib2/src/org/netbeans/modules/editor/lib2/view/TextLayoutCache.java b/editor.lib2/src/org/netbeans/modules/editor/lib2/view/TextLayoutCache.java --- a/editor.lib2/src/org/netbeans/modules/editor/lib2/view/TextLayoutCache.java +++ b/editor.lib2/src/org/netbeans/modules/editor/lib2/view/TextLayoutCache.java @@ -54,7 +54,7 @@ /** * Cache containing paragraph-view references of lines where text layouts * are actively held or where children are actively maintained. - *
      + *
      * This class is not multi-thread safe. * * @author Miloslav Metelka diff --git a/editor.lib2/src/org/netbeans/modules/editor/lib2/view/TextLayoutUtils.java b/editor.lib2/src/org/netbeans/modules/editor/lib2/view/TextLayoutUtils.java --- a/editor.lib2/src/org/netbeans/modules/editor/lib2/view/TextLayoutUtils.java +++ b/editor.lib2/src/org/netbeans/modules/editor/lib2/view/TextLayoutUtils.java @@ -136,7 +136,7 @@ /** * Get real allocation (possibly not rectangular) of a part of layout. - *
      + *
      * It's used when rendering the text layout for filling background highlights of the view. * * @param length Total number of characters for which the allocation is computed. diff --git a/editor.lib2/src/org/netbeans/modules/editor/lib2/view/ViewBuilder.java b/editor.lib2/src/org/netbeans/modules/editor/lib2/view/ViewBuilder.java --- a/editor.lib2/src/org/netbeans/modules/editor/lib2/view/ViewBuilder.java +++ b/editor.lib2/src/org/netbeans/modules/editor/lib2/view/ViewBuilder.java @@ -117,7 +117,7 @@ /** * Start offset of a before-modOffset local view that may be reused. - *
      + *
      * If set to Integer.MAX_VALUE then no more before-modOffset reusing possible * or bmReusePView has no children. */ @@ -136,7 +136,7 @@ /** * Start offset of an above-modOffset local view that may be reused * (or if no local views exist inside - *
      + *
      * If set to Integer.MAX_VALUE then no more before-modOffset reusing possible * or bmReusePView has no children. */ @@ -186,7 +186,7 @@ /** * Actual local views replace inside currently served paragraph view. - *
      + *
      * It may be equal to firstReplace when replacing inside firstly updated paragraph view. */ private ViewReplace localReplace; diff --git a/editor.lib2/src/org/netbeans/modules/editor/lib2/view/ViewChildren.java b/editor.lib2/src/org/netbeans/modules/editor/lib2/view/ViewChildren.java --- a/editor.lib2/src/org/netbeans/modules/editor/lib2/view/ViewChildren.java +++ b/editor.lib2/src/org/netbeans/modules/editor/lib2/view/ViewChildren.java @@ -52,10 +52,10 @@ /** * Class that manages children of either DocumentView or ParagraphView. - *
      + *
      * For document view the class manages visual spans (end visual offsets). * For paragraphs the class manages end offsets of children as well as their end visual offsets. - *
      + *
      * Generally children of {@link #ParagraphView} manage their raw end offsets * while children of {@link #DocumentView} do not manage them (they use Position objects * to manage its start). @@ -95,7 +95,7 @@ /** * Get view index of first view that "contains" the given offset (starts with it or it's inside) * by examining child views' raw end offsets. - *
      + *
      * This is suitable for paragraph view which manages its views' raw end offsets. * * @param offset offset to search for. diff --git a/editor.lib2/src/org/netbeans/modules/editor/lib2/view/ViewGapStorage.java b/editor.lib2/src/org/netbeans/modules/editor/lib2/view/ViewGapStorage.java --- a/editor.lib2/src/org/netbeans/modules/editor/lib2/view/ViewGapStorage.java +++ b/editor.lib2/src/org/netbeans/modules/editor/lib2/view/ViewGapStorage.java @@ -69,7 +69,7 @@ /** * Start of the visual gap in child views along their major axis. - *
      + *
      * Place it above end of all views initially. */ double visualGapStart; // 8-super + 8 = 16 bytes @@ -86,7 +86,7 @@ /** * Start of the offset gap used for managing end offsets of HighlightsView views. * It is not used for paragraph views. - *
      + *
      * Place it above end of all views initially. */ int offsetGapStart; // 28 + 4 = 32 bytes diff --git a/editor.lib2/src/org/netbeans/modules/editor/lib2/view/ViewHierarchy.java b/editor.lib2/src/org/netbeans/modules/editor/lib2/view/ViewHierarchy.java --- a/editor.lib2/src/org/netbeans/modules/editor/lib2/view/ViewHierarchy.java +++ b/editor.lib2/src/org/netbeans/modules/editor/lib2/view/ViewHierarchy.java @@ -42,9 +42,9 @@ /** * View hierarchy associated with a particular text component (for its whole lifetime). - *
      + *
      * View hierarchy needs to be locked before doing most operations - see {@link #lock() }. - *
      + *
      * If editor view hierarchy is currently not installed into particular text component * (text component's root view is not an instance of DocumentView) * the methods (in LockedViewHierarchy) return default values as described in their documentation. @@ -76,7 +76,7 @@ /** * Get text component that this view hierarchy is associated with. - *
      + *
      * * @return non-null text component. */ @@ -86,10 +86,10 @@ /** * Lock view hierarchy in order to perform operations described in {@link LockedViewHierarchy }. - *
      + *
      * Underlying document of the view hierarchy's text component must be read-locked * to guarantee stability of offsets passed to methods of LockedViewHierarchy. - *
      + *
      * Code example: * // Possible textComponent.getDocument() read-locking * LockedViewHierarchy lvh = ViewHierarchy.get(textComponent).lock(); @@ -109,7 +109,7 @@ /** * Add listener for view hierarchy changes. - *
      + *
      * Listener will be notified on a locked view hierarchy. * * @param l non-null listener. diff --git a/editor.lib2/src/org/netbeans/modules/editor/lib2/view/ViewHierarchyEvent.java b/editor.lib2/src/org/netbeans/modules/editor/lib2/view/ViewHierarchyEvent.java --- a/editor.lib2/src/org/netbeans/modules/editor/lib2/view/ViewHierarchyEvent.java +++ b/editor.lib2/src/org/netbeans/modules/editor/lib2/view/ViewHierarchyEvent.java @@ -42,7 +42,7 @@ /** * View hierarchy event describing view rebuilding or view re-measurement change in view hierarchy. - *
      + *
      * Change may be related to several events:
        *
      • Document modification produces immediate updates to view hierarchy. *
      • @@ -62,10 +62,10 @@ * {@link #changeEndOffset()}) and whether the change produces any changes for y-coordinate related * components (such as error stripe and various side bars). Note that y-coordinate related clients * may completely ignore the change start/end offsets and only take care of y-related change information. - *
        + *
        * If change produces y-coordinate changes the changed visual area corresponds to * <{@link #startY()},{@link #endY()}>. - *
        + *
        * The change may cause rest of the document to move visually down/up which is reflected in {@link #deltaY()} * giving amount of pixels the area starting at {@link #endY()} moves down (negative value means moving up). *

        @@ -73,7 +73,7 @@ *

        * Note that when this event is notified the listeners must make no direct queries to view hierarchy. * They should only mark what has changed and needs to be recomputed and ask later. - *
        + *
        * View hierarchy events are fired rather frequently so the code in listeners should be efficient. *

        * @@ -108,12 +108,12 @@ /** * Start offset of a visual change in view hierarchy. - *
        + *
        * All model-to-view translations between {@link #changeStartOffset()} till {@link #changeEndOffset()} * might shift or change. They could change in x-coordinate and possibly also in y-coordinate in case * they lay between {@link #startY()} and {@link #endY()}. Those below {@link #endY()} have y-coordinate * shifted down by {@link #deltaY()} (may be negative for shifting up). - *
        + *
        * Offset corresponds to a state after possible document modification * (returned by {@link #documentEvent()}. * @@ -125,12 +125,12 @@ /** * End offset of a visual change in view hierarchy. - *
        + *
        * All model-to-view translations between {@link #changeStartOffset()} till {@link #changeEndOffset()} * might shift or change. They could change in x-coordinate and possibly also in y-coordinate in case * they lay between {@link #startY()} and {@link #endY()}. Those below {@link #endY()} have y-coordinate * shifted down by {@link #deltaY()} (may be negative for shifting up). - *
        + *
        * Offset corresponds to a state after possible document modification * (returned by {@link #documentEvent()}. * @@ -142,11 +142,11 @@ /** * Whether this change affects y-coordinate clients or not. - *
        + *
        * Return true if there was at least one paragraph view which changed its visual height. * Such views are be included inside {@link #startY()} and {@link #endY()} interval * and a corresponding {@link #deltaY()} is provided. - *
        + *
        * If the paragraph views retain their original offset boundaries upon rebuild * (and their precise span is not computed) they are not reported * as being y-changed. @@ -159,10 +159,10 @@ /** * Where the y-coordinate change begins. - *
        + *
        * Previously computed model-to-view values that have their y-coordinate * within {@link #startY()} and {@link #endY()} interval may be affected. - *
        + *
        * Measurements below {@link #endY()} should shift * its y-coordinate down by {@link #deltaY()} (may be negative for shifting up). * @@ -174,10 +174,10 @@ /** * Where the y-coordinate change ends. - *
        + *
        * Previously computed model-to-view values that have their y-coordinate * within {@link #startY()} and {@link #endY()} interval may be affected. - *
        + *
        * Measurements below {@link #endY()} should shift * its y-coordinate down by {@link #deltaY()} (may be negative for shifting up). * @@ -189,7 +189,7 @@ /** * Shift of area starting at {@link #endY()} down (or up if negative). - *
        + *
        * Measurements below {@link #endY()} should shift * its y-coordinate down by {@link #deltaY()} (may be negative for shifting up). * diff --git a/editor.lib2/src/org/netbeans/modules/editor/lib2/view/ViewHierarchyImpl.java b/editor.lib2/src/org/netbeans/modules/editor/lib2/view/ViewHierarchyImpl.java --- a/editor.lib2/src/org/netbeans/modules/editor/lib2/view/ViewHierarchyImpl.java +++ b/editor.lib2/src/org/netbeans/modules/editor/lib2/view/ViewHierarchyImpl.java @@ -68,9 +68,9 @@ /** * Logger for core operations of the view hierarchy - resolving modelToView() and viewToModel() etc. - *
        + *
        * FINE logs basic info about view hierarchy operations performed. - *
        + *
        * FINER also reports query during document's modification (when the view hierarchy cannot respond * appropriately to queries since it would give incorrect results). */ @@ -79,11 +79,11 @@ /** * Logger tracking all view factory changes that cause either rebuild of the views * or offset repaints. - *
        + *
        * FINE reports which factory reported a change and an offset range of that change. - *
        + *
        * FINER reports additional detailed information about the change. - *
        + *
        * FINEST reports stacktrace where a particular span change request originated. */ static final Logger CHANGE_LOG = Logger.getLogger("org.netbeans.editor.view.change"); // -J-Dorg.netbeans.editor.view.change.level=FINE @@ -100,46 +100,46 @@ /** * Logger for span change requests on the views and underlying text component. - *
        + *
        * FINE reports span change descriptions - *
        + *
        * FINEST reports stacktrace where a particular span change request originated. */ static final Logger SPAN_LOG = Logger.getLogger("org.netbeans.editor.view.span"); // -J-Dorg.netbeans.editor.view.span.level=FINE /** * Logger for repaint requests of the underlying text component. - *
        + *
        * FINE reports repaint request's coordinates - *
        + *
        * FINEST reports stacktrace where a particular repaint request originated. */ static final Logger REPAINT_LOG = Logger.getLogger("org.netbeans.editor.view.repaint"); // -J-Dorg.netbeans.editor.view.repaint.level=FINE /** * Logger for extra consistency checks inside view hierarchy (may slow down processing). - *
        + *
        */ static final Logger CHECK_LOG = Logger.getLogger("org.netbeans.editor.view.check"); // -J-Dorg.netbeans.editor.view.check.level=FINE /** * Logger related to any settings being used in view hierarchy. - *
        + *
        */ static final Logger SETTINGS_LOG = Logger.getLogger("org.netbeans.editor.view.settings"); // -J-Dorg.netbeans.editor.view.settings.level=FINE /** * Logger related to view hierarchy events generation. - *
        + *
        */ static final Logger EVENT_LOG = Logger.getLogger("org.netbeans.editor.view.event"); // -J-Dorg.netbeans.editor.view.event.level=FINE /** * Logger for tracking view hierarchy locking. - *
        + *
        * FINER stores the stack of the lock thread of view hierarchy in lockStack variable * (it should help to find missing unlock). - *
        + *
        * FINEST in addition it dumps thread dump of each locker of a view hierarchy. */ // -J-Dorg.netbeans.modules.editor.lib2.view.ViewHierarchyImpl.level=FINER diff --git a/editor.lib2/src/org/netbeans/modules/editor/lib2/view/ViewHierarchyListener.java b/editor.lib2/src/org/netbeans/modules/editor/lib2/view/ViewHierarchyListener.java --- a/editor.lib2/src/org/netbeans/modules/editor/lib2/view/ViewHierarchyListener.java +++ b/editor.lib2/src/org/netbeans/modules/editor/lib2/view/ViewHierarchyListener.java @@ -50,12 +50,12 @@ /** * Notification about visual change that occurred in view hierarchy. - *
        + *
        * Notification may come in response to document modification but also in response * to a model<->view query to view hierarchy (due to fact that view hierarchy is computed lazily). - *
        + *
        * Notification may come from any thread. - *
        + *
        * When this event is notified the listeners must make no * queries to view hierarchy synchronously (they should only mark what has changed and * ask later). diff --git a/editor.lib2/src/org/netbeans/modules/editor/lib2/view/ViewPaintHighlights.java b/editor.lib2/src/org/netbeans/modules/editor/lib2/view/ViewPaintHighlights.java --- a/editor.lib2/src/org/netbeans/modules/editor/lib2/view/ViewPaintHighlights.java +++ b/editor.lib2/src/org/netbeans/modules/editor/lib2/view/ViewPaintHighlights.java @@ -50,12 +50,12 @@ /** * Special highlights sequence used for painting of individual views. - *
        + *
        * It merges together highlights contained in views (as attributes) together * with extra painting highlights (from highlighting layers that do not change metrics). - *
        + *
        * It "covers" even non-highlighted areas by returning null from {@link #getAttributes()}. - *
        + *
        * The instance can only be used by a single thread. * * @author mmetelka diff --git a/editor.lib2/src/org/netbeans/modules/editor/lib2/view/ViewRenderContext.java b/editor.lib2/src/org/netbeans/modules/editor/lib2/view/ViewRenderContext.java --- a/editor.lib2/src/org/netbeans/modules/editor/lib2/view/ViewRenderContext.java +++ b/editor.lib2/src/org/netbeans/modules/editor/lib2/view/ViewRenderContext.java @@ -47,7 +47,7 @@ /** * Additional info related to view rendering. - *
        + *
        * Provided by {@link EditorView.Parent }. * * @author Miloslav Metelka @@ -72,7 +72,7 @@ /** * Get special highlighting sequence that is a merge of attributes of the - * view with top painting highlights.
        It's only allowed to call this + * view with top painting highlights.
        It's only allowed to call this * method (and use the returned value) during view's paint() methods * execution otherwise it throws an IllegalStateException. * @@ -88,7 +88,7 @@ /** * Get row height of an single row of views being rendered by a paragraph view. - *
        + *
        * For views that only render text the views should return this height * as their vertical span since the user may forcibly decrease row height * (see DocumentViewOp.rowHeightCorrection). @@ -113,7 +113,7 @@ /** * Get font for text rendering that incorporates a possible text zoom * (Alt+MouseWheel function). - *
        + *
        * Ideally all the fonts rendered by views should be "translated" by this method. */ public Font getRenderFont(Font font) { diff --git a/editor.lib2/src/org/netbeans/modules/editor/lib2/view/WrapInfo.java b/editor.lib2/src/org/netbeans/modules/editor/lib2/view/WrapInfo.java --- a/editor.lib2/src/org/netbeans/modules/editor/lib2/view/WrapInfo.java +++ b/editor.lib2/src/org/netbeans/modules/editor/lib2/view/WrapInfo.java @@ -59,7 +59,7 @@ /** * Information about line wrapping that may be attached to {@link ParagraphViewChildren}. - *
        + *
        * Wrapping uses constant wrap line height - children height from {@link ParagraphViewChildren}. * * @author Miloslav Metelka diff --git a/editor.lib2/src/org/netbeans/modules/editor/lib2/view/WrapLine.java b/editor.lib2/src/org/netbeans/modules/editor/lib2/view/WrapLine.java --- a/editor.lib2/src/org/netbeans/modules/editor/lib2/view/WrapLine.java +++ b/editor.lib2/src/org/netbeans/modules/editor/lib2/view/WrapLine.java @@ -68,7 +68,7 @@ /** * Index of a first view located at this line. - *
        + *
        * Logically if there's a non-null startPart then it comes from view * at (firstViewIndex - 1). */ @@ -76,7 +76,7 @@ /** * Index that follows last view located at this line. - *
        + *
        * It should be >= firstViewIndex. */ int endViewIndex; diff --git a/editor.lib2/src/org/netbeans/spi/editor/AbstractEditorAction.java b/editor.lib2/src/org/netbeans/spi/editor/AbstractEditorAction.java --- a/editor.lib2/src/org/netbeans/spi/editor/AbstractEditorAction.java +++ b/editor.lib2/src/org/netbeans/spi/editor/AbstractEditorAction.java @@ -74,38 +74,38 @@ /** * Base class for editor actions that should be used together with * {@link EditorActionRegistration} annotation. - *
        + *
        * It may be constructed and used in two ways: direct construction or construction * upon invocation by a wrapper action: *
          *
        • Direct construction - action is created directly when an editor kit * gets constructed (its kit.getActions() gets used). - *
          + *
          * Advantages: Action controls all its behavior and properties (including enabled status) * since the begining. - *
          + *
          * Disadvantages: Action's class is loaded by classloader at editor kit's construction. - *
          + *
          * Example of registration: - *
          + *
          * - * public static final class MyAction extends AbstractEditorAction {
          - *
          - * @EditorActionRegistration(name = "my-action")
          - * public static MyAction create(Map<String,?> attrs) {
          - * return new MyAction(attrs);
          - * }
          - *
          - * private MyAction(Map<String,?> attrs) {
          - * super(attrs);
          - * ...
          - * }
          - *
          - * protected void actionPerformed(ActionEvent evt, JTextComponent component) {
          - * ...
          - * }
          - *
          - * }
          + * public static final class MyAction extends AbstractEditorAction {
          + *
          + * @EditorActionRegistration(name = "my-action")
          + * public static MyAction create(Map<String,?> attrs) {
          + * return new MyAction(attrs);
          + * }
          + *
          + * private MyAction(Map<String,?> attrs) {
          + * super(attrs);
          + * ...
          + * }
          + *
          + * protected void actionPerformed(ActionEvent evt, JTextComponent component) {
          + * ...
          + * }
          + *
          + * }
          *
          *
        • * @@ -115,33 +115,33 @@ * (upon {@link Action#actionPerformed(java.awt.event.ActionEvent)} call). * Existing properties of the wrapper action (including Action.NAME property) * get transferred into delegate action. - *
          + *
          * Advantages: Action's class is only loaded upon action's execution. - *
          + *
          * Disadvantages: Only a limited set of action's properties gets populated * (those defined by {@link EditorActionRegistration}). - *
          + *
          * Example of registration: - *
          + *
          * - * @EditorActionRegistration(name = "my-action")
          - * public static final class MyAction extends AbstractEditorAction {
          - *
          - * public MyAction() {
          - * // Here the properties are not yet set.
          - * }
          - *
          - * @Override
          - * protected void valuesUpdated() {
          - * // Here the wrapper action has transferred all its properties into this action
          - * // so properties like Action.NAME etc. are now populated.
          - * }
          - *
          - * protected void actionPerformed(ActionEvent evt, JTextComponent component) {
          - * ...
          - * }
          - *
          - * }
          + * @EditorActionRegistration(name = "my-action")
          + * public static final class MyAction extends AbstractEditorAction {
          + *
          + * public MyAction() {
          + * // Here the properties are not yet set.
          + * }
          + *
          + * @Override
          + * protected void valuesUpdated() {
          + * // Here the wrapper action has transferred all its properties into this action
          + * // so properties like Action.NAME etc. are now populated.
          + * }
          + *
          + * protected void actionPerformed(ActionEvent evt, JTextComponent component) {
          + * ...
          + * }
          + *
          + * }
          *
          * *
        @@ -155,23 +155,23 @@ /** * Key of {@link String} property containing a localized display name of the action. - *
        + *
        * It may be passed to {@link #getValue(java.lang.String) } to obtain the property value. */ public static final String DISPLAY_NAME_KEY = "displayName"; // (named in sync with AlwaysEnabledAction) NOI18N /** * Key of {@link String} property containing a localized text to be displayed in a main menu for this action. - *
        + *
        * It may be passed to {@link #getValue(java.lang.String) } to obtain the property value. */ public static final String MENU_TEXT_KEY = "menuText"; // (named in sync with AlwaysEnabledAction) NOI18N /** * Key of {@link String} property containing a localized text to be displayed in a popup menu for this action. - *
        + *
        * If this property is not set then {@link #MENU_TEXT_KEY} is attempted. - *
        + *
        * It may be passed to {@link #getValue(java.lang.String) } to obtain the property value. */ public static final String POPUP_TEXT_KEY = "popupText"; // (named in sync with AlwaysEnabledAction) NOI18N @@ -184,7 +184,7 @@ /** * Key of {@link Boolean} property which determines whether icon of this action should be * displayed in menu (false or unset) or not (true). - *
        + *
        * It may be passed to {@link #getValue(java.lang.String) } to obtain the property value. * @since 1.74 */ @@ -193,7 +193,7 @@ /** * Key of {@link Boolean} property which determines if this action should be * displayed in key binding customizer (false or unset) or not (true). - *
        + *
        * It may be passed to {@link #getValue(java.lang.String) } to obtain the property value. * @since 1.74 */ @@ -202,7 +202,7 @@ /** * Key of property containing a List < List < {@link KeyStroke} > > * listing all multi-key bindings by which the action may be invoked. - *
        + *
        * There may be multiple multi-key bindings to invoke a single action e.g. a code completion * may be invoked by Ctrl+SPACE and also Ctrl+'\' * (in fact each of these bindings could also consist of multiple keystrokes). @@ -219,11 +219,11 @@ /** * Key of {@link String} property containing a mime type for which this action * is registered. - *
        + *
        * Note: action's mime-type is not necessarily the same like EditorKit.getContentType() * for which the action was created because the kit may inherit some actions * from a global mime-type "". - *
        + *
        * Value of this property is checked at action's initialization * (it needs to be passed as part of 'attrs' parameter to constructor). * Subsequent modifications of this property should be avoided and they will likely not affect its behavior. @@ -238,7 +238,7 @@ /** * Key of {@link String} property containing a name of a boolean key in preferences in which this action changes settings * (according to {@link #PREFERENCES_NODE_KEY} property). - *
        + *
        * Once this property is set then it's expected that {@link #PREFERENCES_NODE_KEY} is also set * to a valid value and checkbox menu presenter will be used automatically. */ @@ -253,7 +253,7 @@ * Key of {@link Boolean} property determining whether this is just a wrapper action * that is being used until the action needs to be executed. Then the target action * gets created and run. - *
        + *
        * Value of this property is checked at action's initialization * (it needs to be passed as part of 'attrs' parameter to constructor). * Subsequent modifications of this property should be avoided and they will likely not affect its behavior. @@ -290,27 +290,27 @@ * Constructor that takes a map of attributes that are typically obtained * from an xml layer when an action's creation method is annotated with * @EditorActionRegistration. - *
        + *
        * Example: - *
        + *
        * - * public static final class MyAction extends AbstractEditorAction {
        - *
        - * @EditorActionRegistration(name = "my-action")
        - * public static MyAction create(Map<String,?> attrs) {
        - * return new MyAction(attrs);
        - * }
        - *
        - * private MyAction(Map<String,?> attrs) {
        - * super(attrs);
        - * ...
        - * }
        - *
        - * protected void actionPerformed(ActionEvent evt, JTextComponent component) {
        - * ...
        - * }
        - *
        - * }
        + * public static final class MyAction extends AbstractEditorAction {
        + *
        + * @EditorActionRegistration(name = "my-action")
        + * public static MyAction create(Map<String,?> attrs) {
        + * return new MyAction(attrs);
        + * }
        + *
        + * private MyAction(Map<String,?> attrs) {
        + * super(attrs);
        + * ...
        + * }
        + *
        + * protected void actionPerformed(ActionEvent evt, JTextComponent component) {
        + * ...
        + * }
        + *
        + * }
        *
        * * @param attrs non-null attributes that hold action's properties. @@ -330,28 +330,28 @@ * Constructor typically used when action is constructed lazily * upon its performing (the action is always enabled and its properties * are declared in xml layer by annotation processor for @EditorActionRegistration). - *
        + *
        * Example: - *
        + *
        * - * @EditorActionRegistration(name = "my-action")
        - * public static final class MyAction extends AbstractEditorAction {
        - *
        - * public MyAction() {
        - * // Here the properties are not yet set.
        - * }
        - *
        - * @Override
        - * protected void valuesUpdated() {
        - * // Here the wrapper action has transferred all its properties into this action
        - * // so properties like Action.NAME etc. are now populated.
        - * }
        - *
        - * protected void actionPerformed(ActionEvent evt, JTextComponent component) {
        - * ...
        - * }
        - *
        - * }
        + * @EditorActionRegistration(name = "my-action")
        + * public static final class MyAction extends AbstractEditorAction {
        + *
        + * public MyAction() {
        + * // Here the properties are not yet set.
        + * }
        + *
        + * @Override
        + * protected void valuesUpdated() {
        + * // Here the wrapper action has transferred all its properties into this action
        + * // so properties like Action.NAME etc. are now populated.
        + * }
        + *
        + * protected void actionPerformed(ActionEvent evt, JTextComponent component) {
        + * ...
        + * }
        + *
        + * }
        *
        */ protected AbstractEditorAction() { @@ -388,7 +388,7 @@ /** * Reset caret's magic position. - *
        + *
        * Magic caret position is useful when going through empty lines with Down/Up arrow * then the caret returns on original horizontal column when a particular line has sufficient * number of characters. @@ -401,11 +401,11 @@ /** * Get presenter of this action in main menu. - *
        + *
        * Default implementation uses {@link #MENU_TEXT_KEY} for menu item's text * and the presenter is placed in the menu according to rules * given in the corresponding {@link EditorActionRegistration}. - *
        + *
        * Moreover the default presenter is sensitive to currently active text component * and if the active editor kit has that action redefined it uses the active action's * properties for this presenter. @@ -420,7 +420,7 @@ /** * Get presenter of this action in popup menu. - *
        + *
        * Default implementation uses {@link #POPUP_TEXT_KEY} for popup menu item's text * and the presenter is placed in the popup menu according to rules * given in the corresponding {@link EditorActionRegistration}. @@ -563,12 +563,12 @@ /** * This method is called when a value for the given property * was not yet populated. - *
        + *
        * This method is only called once for the given property. Even if this method * returns null for the given property the infrastructure remembers the * returned value and no longer queries this method (the property can still * be modified by {@link #putValue(java.lang.String, java.lang.Object) }.) - *
        + *
        * Calling of this method and remembering of the returned value does not trigger * {@link #firePropertyChange(java.lang.String, java.lang.Object, java.lang.Object) }. * diff --git a/editor.lib2/src/org/netbeans/spi/editor/caret/CaretMoveHandler.java b/editor.lib2/src/org/netbeans/spi/editor/caret/CaretMoveHandler.java new file mode 100644 --- /dev/null +++ b/editor.lib2/src/org/netbeans/spi/editor/caret/CaretMoveHandler.java @@ -0,0 +1,74 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2016 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 2016 Sun Microsystems, Inc. + */ +package org.netbeans.spi.editor.caret; + +import org.netbeans.api.annotations.common.NonNull; +import org.netbeans.api.editor.caret.CaretMoveContext; + +/** + * Handle possible moving of individual carets to new positions or change their selections. + * + * @author Miloslav Metelka + * @since 2.6 + */ +public interface CaretMoveHandler { + + /** + * Possibly move one or more carets to new position or change their selections + * by using methods in the given context. + *
        + * The method will be called with a document lock acquired. + *
        + * The method is allowed to make document mutations in case the caller + * of {@link org.netbeans.api.editor.caret.EditorCaret#moveCarets(CaretMoveHandler) } + * acquired document write-lock. + *
        + * To prevent deadlocks the method should not acquire any additional locks. + *
        + * The method is not allowed to call methods of EditorCaret that mutate its state + * and do nested calls to EditorCaret.moveCarets(). + * + * @param context non-null context containing the manipulation methods. + */ + void moveCarets(@NonNull CaretMoveContext context); + +} diff --git a/editor.lib2/src/org/netbeans/spi/editor/caret/package.html b/editor.lib2/src/org/netbeans/spi/editor/caret/package.html new file mode 100644 --- /dev/null +++ b/editor.lib2/src/org/netbeans/spi/editor/caret/package.html @@ -0,0 +1,82 @@ + + + + + org.netbeans.spi.editor.caret + + + +

        + The Editor Caret SPI contains interface(s) to be implemented by clients + in order to manipulate the editor carets. +

        + +

        Key parts of the SPI

        + +

        + CaretMoveHandler + allows to implement a transaction that will move individual carets or change their selections. +
        + The following code shows how all carets are moved to the + end of the word they are currently on. +

        +
        +  
        +    editorCaret.moveCarets((CaretMoveContext context) -> {
        +        for (CaretInfo ci : context.getOriginalCarets()) {
        +            Position pos = target.getDocument().createPosition(Utilities.getWordEnd(target, ci.getDot()));
        +            context.setDot(ci, pos);
        +        }
        +    });
        +  
        +  
        + +

        Backwards compatibility

        + +

        Use cases

        +

        + Use cases are shown in javadoc documentation of particular methods. +

        + + + diff --git a/editor.lib2/src/org/netbeans/spi/editor/codegen/CodeGenerator.java b/editor.lib2/src/org/netbeans/spi/editor/codegen/CodeGenerator.java --- a/editor.lib2/src/org/netbeans/spi/editor/codegen/CodeGenerator.java +++ b/editor.lib2/src/org/netbeans/spi/editor/codegen/CodeGenerator.java @@ -73,7 +73,7 @@ public void invoke(); /** - * Factory creating code generators.
        The factory instances are looked up + * Factory creating code generators.
        The factory instances are looked up * by the {@link org.netbeans.api.editor.mimelookup.MimeLookup} so they * should be registered in an xml-layer in * Editors/<mime-type>/CodeGenerators directory. diff --git a/editor.lib2/src/org/netbeans/spi/editor/codegen/CodeGeneratorContextProvider.java b/editor.lib2/src/org/netbeans/spi/editor/codegen/CodeGeneratorContextProvider.java --- a/editor.lib2/src/org/netbeans/spi/editor/codegen/CodeGeneratorContextProvider.java +++ b/editor.lib2/src/org/netbeans/spi/editor/codegen/CodeGeneratorContextProvider.java @@ -48,7 +48,7 @@ /** * Serves for adding an additonal content to the context which is passed * as a parameter to the {@link CodeGenerator.Factory#create(org.openide.util.Lookup)} - * method.
        Instances of this interface are looked up by the + * method.
        Instances of this interface are looked up by the * {@link org.netbeans.api.editor.mimelookup.MimeLookup} so they should be * registered in an xml-layer in * Editors/<mime-type>/CodeGeneratorContextProviders directory. diff --git a/editor.lib2/src/org/netbeans/spi/editor/document/OnSaveTask.java b/editor.lib2/src/org/netbeans/spi/editor/document/OnSaveTask.java --- a/editor.lib2/src/org/netbeans/spi/editor/document/OnSaveTask.java +++ b/editor.lib2/src/org/netbeans/spi/editor/document/OnSaveTask.java @@ -68,7 +68,7 @@ * Perform the given runnable under a lock specific for this task. * The runnable will include a call to {@link #performTask() } but it may * also call other tasks if there are multiple ones. - *
        + *
        * For multiple task factories registered their {@link #runLocked(java.lang.Runnable) } * methods will be called subsequently (according to their registration order) in a nested way. * @@ -87,8 +87,8 @@ /** * Factory for creation of on-save task. - * It should be registered in MimeLookup by using mime registration e.g.
        - * @MimeRegistration(mimeType="", service=OnSaveTask.Factory.class, position=300)
        + * It should be registered in MimeLookup by using mime registration e.g.
        + * @MimeRegistration(mimeType="", service=OnSaveTask.Factory.class, position=300)
        * Optional 'position' parameter may be used to force an order of tasks. * Currently there are two default global factories: *
          @@ -139,10 +139,10 @@ /** * Task may add a custom undoable edit related to its operation by using this method. - *
          + *
          * When undo would be performed after the save then this edit would be undone * (together with any possible modification changes performed by the task on the underlying document). - *
          + *
          * Note: this method should only be called during {@link OnSaveTask#performTask() }. * * @param edit a custom undoable edit provided by the task. @@ -159,9 +159,9 @@ /** * Get a root element with zero or more child elements each designating a modified region * of a document. - *
          + *
          * Tasks may use this information to work on modified document parts only. - *
          + *
          * Note: unlike in some other root element implementations here the child elements * do not fully cover the root element's offset space. * diff --git a/editor.lib2/src/org/netbeans/spi/editor/highlighting/ShiftHighlightsSequence.java b/editor.lib2/src/org/netbeans/spi/editor/highlighting/ShiftHighlightsSequence.java new file mode 100644 --- /dev/null +++ b/editor.lib2/src/org/netbeans/spi/editor/highlighting/ShiftHighlightsSequence.java @@ -0,0 +1,76 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2016 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 2016 Sun Microsystems, Inc. + */ +package org.netbeans.spi.editor.highlighting; + +/** + * Highlights sequence that supports shifts in addition to regular offsets. + * This allows to color individual spaces within a tab character + * or to color extra virtual characters beyond a newline character. + * + * @author Miloslav Metelka + * @since 2.5.0 + */ +public interface ShiftHighlightsSequence extends HighlightsSequence { + + /** + * Get extra shift "within" a particular character (either tab or newline) + * while {@link #getStartOffset()} points to the tab or newline character. + *
          + * To highlight second and third space of a tab character at offset == 123 + * the {@link #getStartOffset() } == {@link #getEndOffset() } == 123 + * and {@link #getStartShift() } == 1 and {@link #getEndShift() } == 3. + * + * @return >=0 start shift. + * @see #getStartOffset() + */ + int getStartShift(); + + /** + * Get end shift of a highlight "within" a particular character (either tab or newline) + * while {@link #getEndOffset()} points to the tab or newline character. + * + * @return >=0 end shift. + * @see #getStartShift() + */ + int getEndShift(); + +} diff --git a/editor.lib2/src/org/netbeans/spi/editor/typinghooks/TypedBreakInterceptor.java b/editor.lib2/src/org/netbeans/spi/editor/typinghooks/TypedBreakInterceptor.java --- a/editor.lib2/src/org/netbeans/spi/editor/typinghooks/TypedBreakInterceptor.java +++ b/editor.lib2/src/org/netbeans/spi/editor/typinghooks/TypedBreakInterceptor.java @@ -97,7 +97,7 @@ * additional rules (eg. correctly replacing selected text, handling insert vs override * modes of the caret, etc). The first interceptor that modifies the insertion text * will win and no other interceptor's insert method will be called. - *
          + *
          * The interceptors are allowed to insert more than just a line break, but the text * they insert has to contain at least one line break. They can also request the * inserted text to be reindented. Please see {@link MutableContext#setText(java.lang.String, int, int, int...)} diff --git a/editor.lib2/test/unit/src/org/netbeans/modules/editor/lib2/highlighting/DirectMergeContainerTest.java b/editor.lib2/test/unit/src/org/netbeans/modules/editor/lib2/highlighting/DirectMergeContainerTest.java --- a/editor.lib2/test/unit/src/org/netbeans/modules/editor/lib2/highlighting/DirectMergeContainerTest.java +++ b/editor.lib2/test/unit/src/org/netbeans/modules/editor/lib2/highlighting/DirectMergeContainerTest.java @@ -83,11 +83,17 @@ @Test public void testRandomMerges() throws Exception { RandomTestContainer container = HighlightsMergeTesting.createContainer(); + container.setName("testRandomMerges"); RandomTestContainer.Round round = HighlightsMergeTesting.addRound(container); round.setOpCount(100); - container.setLogOp(false); - container.run(1303832573413L); - container.run(0L); +// container.setLogOp(true); +// HighlightsMergeTesting.setLogChecks(true); + container.runInit(1303832573413L); + container.runOps(1); + container.runOps(1); + container.runOps(0); // Run till end + +// container.run(0L); } } diff --git a/editor.lib2/test/unit/src/org/netbeans/modules/editor/lib2/highlighting/HighlightsMergeTesting.java b/editor.lib2/test/unit/src/org/netbeans/modules/editor/lib2/highlighting/HighlightsMergeTesting.java --- a/editor.lib2/test/unit/src/org/netbeans/modules/editor/lib2/highlighting/HighlightsMergeTesting.java +++ b/editor.lib2/test/unit/src/org/netbeans/modules/editor/lib2/highlighting/HighlightsMergeTesting.java @@ -82,6 +82,12 @@ private static final int DOCUMENT_LENGTH = 1000; // Fixed document length private static final int MAX_LAYER_HIGHLIGHT_COUNT = 20; + + private static boolean logChecks; + + public static void setLogChecks(boolean logChecks) { + HighlightsMergeTesting.logChecks = logChecks; + } public static RandomTestContainer createContainer() { RandomTestContainer container = new RandomTestContainer(); @@ -147,21 +153,6 @@ Highlight highlight = new Highlight(offset0, offset1, (AttributeSet)highlights[i+2]); highlightList.add(highlight); } - // Possibly do logging - if (context.isLogOp()) { - StringBuilder sb = context.logOpBuilder(); - sb.append(" ADD_LAYER(").append(zIndex).append("): "); - sb.append('\n'); - int digitCount = ArrayUtilities.digitCount(highlightList.size()); - for (int i = 0; i < highlightList.size(); i++) { - Highlight hi = highlightList.get(i); - ArrayUtilities.appendBracketedIndex(sb, i, digitCount); - sb.append(hi); - sb.append('\n'); - } - context.logOp(sb); - } - HighlightsContainer[] layers = compoundHighlightsContainer.getLayers(); HighlightsContainer[] newLayers = new HighlightsContainer[layers.length + 1]; zIndex = Math.min(zIndex, layers.length); @@ -172,6 +163,33 @@ highlight.addTo(bag); } newLayers[zIndex] = bag; + + // Possibly do logging + if (context.isLogOp()) { + StringBuilder sb = context.logOpBuilder(); + sb.append(" ADD_LAYER(").append(zIndex).append("): "); + sb.append('\n'); + int digitCount = ArrayUtilities.digitCount(highlightList.size()); + for (int i = 0; i < highlightList.size(); i++) { + Highlight hi = highlightList.get(i); + sb.append("Parameter highlight"); + ArrayUtilities.appendBracketedIndex(sb, i, digitCount); + sb.append(hi); + sb.append('\n'); + } + HighlightsSequence hs = bag.getHighlights(0, Integer.MAX_VALUE); + int i = 0; + while (hs.moveNext()) { + int startOffset = hs.getStartOffset(); + int endOffset = hs.getEndOffset(); + AttributeSet attrs = hs.getAttributes(); + sb.append("Bag highlight"); + ArrayUtilities.appendBracketedIndex(sb, i, digitCount); // May be actually more/less digits due to splitting/merging + sb.append(new Highlight(startOffset, endOffset, attrs)); + sb.append('\n'); + } + context.logOp(sb); + } compoundHighlightsContainer.setLayers(doc, newLayers); DirectMergeContainer directMergeContainer = directMergeContainer(context); directMergeContainer = new DirectMergeContainer(newLayers); @@ -210,6 +228,7 @@ int startOffset = 0; int endOffset = 0; AttributeSet attrs = null; + int i = 0; while (expectedSeq.moveNext()) { startOffset = expectedSeq.getStartOffset(); endOffset = expectedSeq.getEndOffset(); @@ -226,9 +245,14 @@ assert (attrs.equals(testAttrs)) : "attrs=" + attrs + " != testAttrs=" + testAttrs + ", startOffset=" + startOffset + ", endOffset=" + endOffset + " seq: " + testSeq; if (logChecks) { - StringBuilder sb = context.logOpBuilder().append("Passed: ").append(new Highlight(startOffset, endOffset, attrs)); + StringBuilder sb = context.logOpBuilder(); + sb.append("DMContainer passed highlight"); + ArrayUtilities.appendBracketedIndex(sb, i, 1); // Unknown digit count + sb.append(new Highlight(startOffset, endOffset, attrs)); + sb.append('\n'); context.logOp(sb); } + i++; } } @@ -286,7 +310,7 @@ @Override protected void check(Context context) throws Exception { - checkMerge(context, false); + checkMerge(context, HighlightsMergeTesting.logChecks); } } diff --git a/editor.search/nbproject/project.xml b/editor.search/nbproject/project.xml --- a/editor.search/nbproject/project.xml +++ b/editor.search/nbproject/project.xml @@ -117,14 +117,6 @@ - org.openide.util.ui - - - - 9.3 - - - org.openide.util @@ -140,6 +132,14 @@ 8.12 + + org.openide.util.ui + + + + 9.3 + + diff --git a/editor.search/src/org/netbeans/modules/editor/search/Bundle.properties b/editor.search/src/org/netbeans/modules/editor/search/Bundle.properties --- a/editor.search/src/org/netbeans/modules/editor/search/Bundle.properties +++ b/editor.search/src/org/netbeans/modules/editor/search/Bundle.properties @@ -6,8 +6,10 @@ TOOLTIP_CloseIncrementalSearchSidebar=Close Incremental Search Sidebar (Esc) CTL_Find=&Find: TOOLTIP_IncrementalSearchText=Search Text (ENTER - Find Next, Shift+ENTER - Find Previous) +TOOLTIP_SelectAllText=Place a selection around all occurrences in the editor TOOLTIP_ReplaceText=Replace Text (ENTER - Replace, Shift+ENTER - Replace All) CTL_FindNext=Next +CTL_SelectAll=Select CTL_FindPrevious=Previous TT_MatchCase=Match case (Alt + C) TT_WholeWords=Whole word (Alt + O) diff --git a/editor.search/src/org/netbeans/modules/editor/search/EditorFindSupport.java b/editor.search/src/org/netbeans/modules/editor/search/EditorFindSupport.java --- a/editor.search/src/org/netbeans/modules/editor/search/EditorFindSupport.java +++ b/editor.search/src/org/netbeans/modules/editor/search/EditorFindSupport.java @@ -76,11 +76,13 @@ import org.netbeans.api.editor.settings.FontColorNames; import org.netbeans.api.editor.settings.SimpleValueNames; import org.netbeans.api.editor.NavigationHistory; +import org.netbeans.api.editor.caret.EditorCaret; import org.netbeans.modules.editor.lib2.ComponentUtils; import org.netbeans.modules.editor.lib2.DocUtils; import org.netbeans.modules.editor.lib2.highlighting.BlockHighlighting; import org.netbeans.modules.editor.lib2.highlighting.Factory; import org.netbeans.modules.editor.search.DocumentFinder.FindReplaceResult; +import org.openide.util.Exceptions; import org.openide.util.NbBundle; /** @@ -131,6 +133,7 @@ public static final String FIND_BLOCK_SEARCH = "find-block-search"; //NOI18N public static final String FIND_BLOCK_SEARCH_START = "find-block-search-start"; //NOI18N public static final String FIND_BLOCK_SEARCH_END = "find-block-search-end"; //NOI18N + public static final String ADD_MULTICARET = "add-multi-caret"; //NOI18N private static final String FOUND_LOCALE = "find-found"; // NOI18N private static final String NOT_FOUND_LOCALE = "find-not-found"; // NOI18N @@ -203,6 +206,7 @@ props.put(FIND_REG_EXP, Boolean.FALSE); props.put(FIND_HISTORY, Integer.valueOf(30)); props.put(FIND_PRESERVE_CASE, Boolean.FALSE); + props.put(ADD_MULTICARET, Boolean.FALSE); return props; } @@ -440,6 +444,19 @@ } return back; } + + private void addCaretSelectText(JTextComponent c, int start, int end, boolean back) { + Caret eCaret = c.getCaret(); + ensureVisible(c, start, end); + if (eCaret instanceof EditorCaret) { + EditorCaret caret = (EditorCaret) eCaret; + try { + caret.addCaret(c.getDocument().createPosition(end), c.getDocument().createPosition(start)); + } catch (BadLocationException ex) { + Exceptions.printStackTrace(ex); + } + } + } private void selectText(JTextComponent c, int start, int end, boolean back){ Caret caret = c.getCaret(); @@ -585,7 +602,11 @@ blk = result.getFoundPositions(); } if (blk != null) { - selectText(c, blk[0], blk[1], back); + if (Boolean.TRUE.equals(props.get(EditorFindSupport.ADD_MULTICARET))) { + addCaretSelectText(c, blk[0], blk[1], back); + } else { + selectText(c, blk[0], blk[1], back); + } String msg = NbBundle.getMessage(EditorFindSupport.class, FOUND_LOCALE, findWhat, DocUtils.debugPosition(c.getDocument(), Integer.valueOf(blk[0]))); // String msg = exp + NbBundle.getMessage(EditorFindSupport.class, FOUND_LOCALE) // + ' ' + DocUtils.debugPosition(c.getDocument(), blk[0]); @@ -609,7 +630,7 @@ EditorFindSupport.class, NOT_FOUND_LOCALE, findWhat), IMPORTANCE_FIND_OR_REPLACE); // issue 14189 - selection was not removed c.getCaret().setDot(c.getCaret().getDot()); - } + } } catch (BadLocationException e) { LOG.log(Level.WARNING, e.getMessage(), e); } diff --git a/editor.search/src/org/netbeans/modules/editor/search/SearchBar.java b/editor.search/src/org/netbeans/modules/editor/search/SearchBar.java --- a/editor.search/src/org/netbeans/modules/editor/search/SearchBar.java +++ b/editor.search/src/org/netbeans/modules/editor/search/SearchBar.java @@ -65,6 +65,7 @@ import org.netbeans.api.editor.mimelookup.MimeLookup; import org.netbeans.api.editor.mimelookup.MimePath; import org.netbeans.api.editor.settings.SimpleValueNames; +import org.netbeans.api.editor.caret.EditorCaret; import org.netbeans.api.search.ReplacePattern; import org.netbeans.api.search.SearchHistory; import org.netbeans.api.search.SearchPattern; @@ -79,6 +80,7 @@ import org.openide.filesystems.FileUtil; import org.openide.util.Exceptions; import org.openide.util.NbBundle; +import org.openide.util.Pair; import org.openide.util.WeakListeners; /** @@ -109,6 +111,7 @@ private boolean hadFocusOnIncSearchTextField = false; private final JButton findNextButton; private final JButton findPreviousButton; + private final JButton selectAllButton; private final JToggleButton matchCase; private final JToggleButton wholeWords; private final JToggleButton regexp; @@ -164,6 +167,7 @@ incSearchTextField.getDocument().addDocumentListener(incSearchTextFieldListener); addEnterKeystrokeFindNextTo(incSearchTextField); addShiftEnterKeystrokeFindPreviousTo(incSearchTextField); + addAltEnterKeystrokeSelect(incSearchTextField); if (getCurrentKeyMapProfile().startsWith("Emacs")) { // NOI18N emacsProfileFix(incSearchTextField); } @@ -205,6 +209,17 @@ }); add(findNextButton); + selectAllButton = SearchButton.createButton("org/netbeans/modules/editor/search/resources/select_all.png", "CTL_SelectAll"); // NOI18N + selectAllButton.setToolTipText(NbBundle.getMessage(SearchBar.class, "TOOLTIP_SelectAllText")); //NOI18N + selectAllButton.addActionListener(new ActionListener() { + + @Override + public void actionPerformed(ActionEvent e) { + selectAll(); + } + }); + add(selectAllButton); + final JToolBar.Separator rightSeparator = new JToolBar.Separator(); rightSeparator.setOrientation(SwingConstants.VERTICAL); add(rightSeparator); @@ -434,7 +449,9 @@ @Override public void keyPressed(KeyEvent e) { if (e.getKeyCode() == KeyEvent.VK_ESCAPE) { - looseFocus(); + if(looseFocus()) { + e.consume(); + }; ReplaceBar replaceBarInstance = ReplaceBar.getInstance(SearchBar.this); if (replaceBarInstance.isVisible()) { replaceBarInstance.looseFocus(); @@ -540,6 +557,20 @@ return pcl; } + private void addAltEnterKeystrokeSelect(JTextComponent incSearchTextField) { + incSearchTextField.getInputMap().put(KeyStroke.getKeyStroke( + KeyEvent.VK_ENTER, InputEvent.ALT_MASK, true), + "select-all"); // NOI18N + incSearchTextField.getActionMap().put("select-all", // NOI18N + new AbstractAction() { + + @Override + public void actionPerformed(ActionEvent e) { + selectAll(); + } + }); + } + private void addShiftEnterKeystrokeFindPreviousTo(JTextComponent incSearchTextField) { incSearchTextField.getInputMap().put(KeyStroke.getKeyStroke( KeyEvent.VK_ENTER, InputEvent.SHIFT_MASK, true), @@ -811,10 +842,10 @@ incSearchTextField.getDocument().addDocumentListener(incSearchTextFieldListener); } - public void looseFocus() { + public boolean looseFocus() { hadFocusOnIncSearchTextField = false; if (!isVisible()) { - return; + return false; } EditorFindSupport.getInstance().setBlockSearchHighlight(0, 0); EditorFindSupport.getInstance().incSearchReset(); @@ -839,6 +870,7 @@ searchProps.saveToPrefs(); } }); + return true; } private void incrementalSearch() { @@ -891,6 +923,39 @@ void findPrevious() { find(false); } + + void selectAll() { + EditorFindSupport findSupport = EditorFindSupport.getInstance(); + JTextComponent textComponent = getActualTextComponent(); + Document doc = textComponent.getDocument(); + Caret caret = textComponent.getCaret(); + if(caret instanceof EditorCaret) { + EditorCaret editorCaret = (EditorCaret) caret; + try { + int[] blocks = findSupport.getBlocks(new int[]{-1, -1}, doc, 0, doc.getLength()); + if(blocks[0] >= 0 && blocks.length % 2 == 0) { + List newCarets = new ArrayList<>(editorCaret.getCarets().size() << 1); + for (int i = 0; i < blocks.length; i += 2) { + int start = blocks[i]; + int end = blocks[i+1]; + if(start == -1 || end == -1) { + break; + } + Position startPos = doc.createPosition(start); + Position endPos = doc.createPosition(end); + newCarets.add(endPos); + newCarets.add(startPos); + } + + editorCaret.replaceCarets(newCarets); + + textComponent.requestFocusInWindow(); + } + } catch (BadLocationException ex) { + Exceptions.printStackTrace(ex); + } + } + } public int getNumOfMatches() { return numOfMatches; @@ -1221,4 +1286,4 @@ String lfID = laf.getID(); return lf.equals(lfID); } -} \ No newline at end of file +} diff --git a/editor.search/src/org/netbeans/modules/editor/search/actions/FindSelectionAction.java b/editor.search/src/org/netbeans/modules/editor/search/actions/AddCaretSelectAllAction.java copy from editor.search/src/org/netbeans/modules/editor/search/actions/FindSelectionAction.java copy to editor.search/src/org/netbeans/modules/editor/search/actions/AddCaretSelectAllAction.java --- a/editor.search/src/org/netbeans/modules/editor/search/actions/FindSelectionAction.java +++ b/editor.search/src/org/netbeans/modules/editor/search/actions/AddCaretSelectAllAction.java @@ -1,5 +1,3 @@ -package org.netbeans.modules.editor.search.actions; - /* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * @@ -42,33 +40,40 @@ * Portions Copyrighted 2013 Sun Microsystems, Inc. */ +package org.netbeans.modules.editor.search.actions; import java.awt.event.ActionEvent; import java.util.HashMap; -import java.util.Map; +import java.util.ArrayList; +import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.text.BadLocationException; import javax.swing.text.Caret; +import javax.swing.text.Document; import javax.swing.text.JTextComponent; +import javax.swing.text.Position; import org.netbeans.api.editor.EditorActionRegistration; +import org.netbeans.api.editor.caret.EditorCaret; import org.netbeans.editor.BaseDocument; -import org.netbeans.editor.BaseKit; import org.netbeans.editor.EditorUI; import org.netbeans.editor.Utilities; import org.netbeans.modules.editor.search.EditorFindSupport; import org.netbeans.spi.editor.AbstractEditorAction; +import org.openide.util.Exceptions; +import org.openide.util.Pair; -/** Finds either selection or if there's no selection it finds - * the word where the cursor is standing. +/** + * Select */ // NOI18N -@EditorActionRegistration(name = BaseKit.findSelectionAction, iconResource = "org/netbeans/modules/editor/search/resources/find_selection.png") // NOI18N -public class FindSelectionAction extends AbstractEditorAction { - static final long serialVersionUID = -5601618936504699565L; - private static final Logger LOGGER = Logger.getLogger(FindSelectionAction.class.getName()); +@EditorActionRegistration( + name = "addCaretSelectAll") +public class AddCaretSelectAllAction extends AbstractEditorAction { - public FindSelectionAction() { + private static final Logger LOGGER = Logger.getLogger(AddCaretSelectAllAction.class.getName()); + + public AddCaretSelectAllAction() { super(); } @@ -76,63 +81,61 @@ @SuppressWarnings("unchecked") public void actionPerformed(ActionEvent evt, JTextComponent target) { if (target != null) { - EditorFindSupport findSupport = EditorFindSupport.getInstance(); Caret caret = target.getCaret(); - int dotPos = caret.getDot(); - HashMap props = new HashMap<>(findSupport.createDefaultFindProperties()); - String searchWord = null; - boolean revert = false; - Boolean originalValue = null; - Map revertMap = (Map) props.get(EditorFindSupport.REVERT_MAP); - Boolean revertValue = revertMap != null ? (Boolean) revertMap.get(EditorFindSupport.FIND_WHOLE_WORDS) : null; - if (Utilities.isSelectionShowing(caret)) { - // valid selection - searchWord = target.getSelectedText(); - originalValue = (Boolean) props.put(EditorFindSupport.FIND_WHOLE_WORDS, Boolean.FALSE); - if (Boolean.FALSE.equals(revertValue)) { - revertMap.remove(EditorFindSupport.FIND_WHOLE_WORDS); - } else { - revert = !Boolean.FALSE.equals(originalValue); - } - } else { - // no selection, get current word + if (!Utilities.isSelectionShowing(caret)) { try { - searchWord = Utilities.getIdentifier((BaseDocument) target.getDocument(), dotPos); - originalValue = (Boolean) props.put(EditorFindSupport.FIND_WHOLE_WORDS, Boolean.TRUE); - if (Boolean.TRUE.equals(revertValue)) { - revertMap.remove(EditorFindSupport.FIND_WHOLE_WORDS); - } else { - revert = !Boolean.TRUE.equals(originalValue); + int[] identifierBlock = Utilities.getIdentifierBlock((BaseDocument) target.getDocument(), caret.getDot()); + if (identifierBlock != null) { + caret.setDot(identifierBlock[0]); + caret.moveDot(identifierBlock[1]); } } catch (BadLocationException e) { LOGGER.log(Level.WARNING, null, e); } } - if (searchWord != null) { - int n = searchWord.indexOf('\n'); - if (n >= 0) { - searchWord = searchWord.substring(0, n); - } - props.put(EditorFindSupport.FIND_WHAT, searchWord); - if (revert) { - revertMap = new HashMap<>(); - revertMap.put(EditorFindSupport.FIND_WHOLE_WORDS, originalValue != null ? originalValue : Boolean.FALSE); - props.put(EditorFindSupport.REVERT_MAP, revertMap); - } - props.put(EditorFindSupport.FIND_BLOCK_SEARCH, Boolean.FALSE); - props.put(EditorFindSupport.FIND_BLOCK_SEARCH_START, null); - props.put(EditorFindSupport.FIND_BLOCK_SEARCH_END, null); - EditorUI eui = org.netbeans.editor.Utilities.getEditorUI(target); - if (eui.getComponent().getClientProperty("AsTextField") == null) { - //NOI18N - findSupport.setFocusedTextComponent(eui.getComponent()); - } - findSupport.putFindProperties(props); - if (findSupport.find(null, false)) { - findSupport.addToHistory(new EditorFindSupport.SPW((String) props.get(EditorFindSupport.FIND_WHAT), (Boolean) props.get(EditorFindSupport.FIND_WHOLE_WORDS), (Boolean) props.get(EditorFindSupport.FIND_MATCH_CASE), (Boolean) props.get(EditorFindSupport.FIND_REG_EXP))); + + EditorFindSupport findSupport = EditorFindSupport.getInstance(); + HashMap props = new HashMap<>(findSupport.createDefaultFindProperties()); + String searchWord = target.getSelectedText(); + int n = searchWord.indexOf('\n'); + if (n >= 0) { + searchWord = searchWord.substring(0, n); + } + props.put(EditorFindSupport.FIND_WHAT, searchWord); + Document doc = target.getDocument(); + EditorUI eui = org.netbeans.editor.Utilities.getEditorUI(target); + if (eui.getComponent().getClientProperty("AsTextField") == null) { //NOI18N + findSupport.setFocusedTextComponent(eui.getComponent()); + } + findSupport.putFindProperties(props); + findSupport.find(null, false); + + if (caret instanceof EditorCaret) { + EditorCaret editorCaret = (EditorCaret) caret; + try { + int[] blocks = findSupport.getBlocks(new int[]{-1, -1}, doc, 0, doc.getLength()); + if (blocks[0] >= 0 && blocks.length % 2 == 0) { + List newCarets = new ArrayList<>(); + + for (int i = 0; i < blocks.length; i += 2) { + int start = blocks[i]; + int end = blocks[i + 1]; + if (start == -1 || end == -1) { + break; + } + Position startPos = doc.createPosition(start); + Position endPos = doc.createPosition(end); + newCarets.add(endPos); + newCarets.add(startPos); + } + + editorCaret.replaceCarets(newCarets); + } + } catch (BadLocationException ex) { + Exceptions.printStackTrace(ex); } } + } } - } diff --git a/editor.search/src/org/netbeans/modules/editor/search/actions/FindSelectionAction.java b/editor.search/src/org/netbeans/modules/editor/search/actions/AddCaretSelectNextAction.java copy from editor.search/src/org/netbeans/modules/editor/search/actions/FindSelectionAction.java copy to editor.search/src/org/netbeans/modules/editor/search/actions/AddCaretSelectNextAction.java --- a/editor.search/src/org/netbeans/modules/editor/search/actions/FindSelectionAction.java +++ b/editor.search/src/org/netbeans/modules/editor/search/actions/AddCaretSelectNextAction.java @@ -1,5 +1,3 @@ -package org.netbeans.modules.editor.search.actions; - /* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * @@ -42,10 +40,10 @@ * Portions Copyrighted 2013 Sun Microsystems, Inc. */ +package org.netbeans.modules.editor.search.actions; import java.awt.event.ActionEvent; import java.util.HashMap; -import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.text.BadLocationException; @@ -53,22 +51,21 @@ import javax.swing.text.JTextComponent; import org.netbeans.api.editor.EditorActionRegistration; import org.netbeans.editor.BaseDocument; -import org.netbeans.editor.BaseKit; import org.netbeans.editor.EditorUI; import org.netbeans.editor.Utilities; import org.netbeans.modules.editor.search.EditorFindSupport; import org.netbeans.spi.editor.AbstractEditorAction; -/** Finds either selection or if there's no selection it finds - * the word where the cursor is standing. +/** + * Select a whole word if there is not selection on word otherwise it adds a new caret for selected text */ // NOI18N -@EditorActionRegistration(name = BaseKit.findSelectionAction, iconResource = "org/netbeans/modules/editor/search/resources/find_selection.png") // NOI18N -public class FindSelectionAction extends AbstractEditorAction { - static final long serialVersionUID = -5601618936504699565L; - private static final Logger LOGGER = Logger.getLogger(FindSelectionAction.class.getName()); +@EditorActionRegistration( + name = "addCaretSelectNext") +public class AddCaretSelectNextAction extends AbstractEditorAction { + private static final Logger LOGGER = Logger.getLogger(AddCaretSelectNextAction.class.getName()); - public FindSelectionAction() { + public AddCaretSelectNextAction() { super(); } @@ -76,60 +73,32 @@ @SuppressWarnings("unchecked") public void actionPerformed(ActionEvent evt, JTextComponent target) { if (target != null) { - EditorFindSupport findSupport = EditorFindSupport.getInstance(); Caret caret = target.getCaret(); - int dotPos = caret.getDot(); - HashMap props = new HashMap<>(findSupport.createDefaultFindProperties()); - String searchWord = null; - boolean revert = false; - Boolean originalValue = null; - Map revertMap = (Map) props.get(EditorFindSupport.REVERT_MAP); - Boolean revertValue = revertMap != null ? (Boolean) revertMap.get(EditorFindSupport.FIND_WHOLE_WORDS) : null; if (Utilities.isSelectionShowing(caret)) { - // valid selection - searchWord = target.getSelectedText(); - originalValue = (Boolean) props.put(EditorFindSupport.FIND_WHOLE_WORDS, Boolean.FALSE); - if (Boolean.FALSE.equals(revertValue)) { - revertMap.remove(EditorFindSupport.FIND_WHOLE_WORDS); - } else { - revert = !Boolean.FALSE.equals(originalValue); - } - } else { - // no selection, get current word - try { - searchWord = Utilities.getIdentifier((BaseDocument) target.getDocument(), dotPos); - originalValue = (Boolean) props.put(EditorFindSupport.FIND_WHOLE_WORDS, Boolean.TRUE); - if (Boolean.TRUE.equals(revertValue)) { - revertMap.remove(EditorFindSupport.FIND_WHOLE_WORDS); - } else { - revert = !Boolean.TRUE.equals(originalValue); - } - } catch (BadLocationException e) { - LOGGER.log(Level.WARNING, null, e); - } - } - if (searchWord != null) { + EditorFindSupport findSupport = EditorFindSupport.getInstance(); + HashMap props = new HashMap<>(findSupport.createDefaultFindProperties()); + String searchWord = target.getSelectedText(); int n = searchWord.indexOf('\n'); if (n >= 0) { searchWord = searchWord.substring(0, n); } props.put(EditorFindSupport.FIND_WHAT, searchWord); - if (revert) { - revertMap = new HashMap<>(); - revertMap.put(EditorFindSupport.FIND_WHOLE_WORDS, originalValue != null ? originalValue : Boolean.FALSE); - props.put(EditorFindSupport.REVERT_MAP, revertMap); - } - props.put(EditorFindSupport.FIND_BLOCK_SEARCH, Boolean.FALSE); - props.put(EditorFindSupport.FIND_BLOCK_SEARCH_START, null); - props.put(EditorFindSupport.FIND_BLOCK_SEARCH_END, null); + props.put(EditorFindSupport.ADD_MULTICARET, Boolean.TRUE); EditorUI eui = org.netbeans.editor.Utilities.getEditorUI(target); - if (eui.getComponent().getClientProperty("AsTextField") == null) { - //NOI18N + if (eui.getComponent().getClientProperty("AsTextField") == null) { //NOI18N findSupport.setFocusedTextComponent(eui.getComponent()); } findSupport.putFindProperties(props); - if (findSupport.find(null, false)) { - findSupport.addToHistory(new EditorFindSupport.SPW((String) props.get(EditorFindSupport.FIND_WHAT), (Boolean) props.get(EditorFindSupport.FIND_WHOLE_WORDS), (Boolean) props.get(EditorFindSupport.FIND_MATCH_CASE), (Boolean) props.get(EditorFindSupport.FIND_REG_EXP))); + findSupport.find(null, false); + } else { + try { + int[] identifierBlock = Utilities.getIdentifierBlock((BaseDocument) target.getDocument(), caret.getDot()); + if (identifierBlock != null) { + caret.setDot(identifierBlock[0]); + caret.moveDot(identifierBlock[1]); + } + } catch (BadLocationException e) { + LOGGER.log(Level.WARNING, null, e); } } } diff --git a/editor.search/src/org/netbeans/modules/editor/search/actions/Bundle.properties b/editor.search/src/org/netbeans/modules/editor/search/actions/Bundle.properties --- a/editor.search/src/org/netbeans/modules/editor/search/actions/Bundle.properties +++ b/editor.search/src/org/netbeans/modules/editor/search/actions/Bundle.properties @@ -5,4 +5,6 @@ find-next=Find Next Occurrence find-previous=Find Previous Occurrence find-selection=Find Selection -toggle-highlight-search=Toggle Highlight Search \ No newline at end of file +toggle-highlight-search=Toggle Highlight Search +addCaretSelectNext=Add Selection For Next Occurrence +addCaretSelectAll=Select All Occurrences \ No newline at end of file diff --git a/editor.search/src/org/netbeans/modules/editor/search/resources/replace.png b/editor.search/src/org/netbeans/modules/editor/search/resources/select_all.png copy from editor.search/src/org/netbeans/modules/editor/search/resources/replace.png copy to editor.search/src/org/netbeans/modules/editor/search/resources/select_all.png index a4e2ce10e6a709e68f0b2eb188fe5f9a4fbc2f1a..8f8dd5f44d200c9c76774be558a71a114c5246ee GIT binary patch literal 250 zc%17D@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Y)RhkE)4%caKYZ?lYt_f1s;*b z3=DjSL74G){)!Z!AbW|YuPgg~9!_q3W&sw3=Rl!$PZ!4!i_^&o60AK2$99FZ{a=#B zH|y+y6HJf){5X0*;8OA2jI-HZCXy0B@PMu6=fMLX{+{$)#QWn@Gvn0;|ND{>|7t2c zwlL1$w^;JSPiyAG_t&rQG<#rtg+Xmc=Y0u@fB%2`PI>nKf1Pp7uQoC6%wrQGethbl psNr03 + + Added methods to append elements to a GapList + + + + + + Added supplementary methods to GapList to accompany the existing + methods to add elements. + + + + Added test for property storage support on events diff --git a/editor.util/manifest.mf b/editor.util/manifest.mf --- a/editor.util/manifest.mf +++ b/editor.util/manifest.mf @@ -2,4 +2,4 @@ OpenIDE-Module: org.netbeans.modules.editor.util/1 OpenIDE-Module-Localizing-Bundle: org/netbeans/lib/editor/util/Bundle.properties AutoUpdate-Show-In-Client: false -OpenIDE-Module-Specification-Version: 1.63 +OpenIDE-Module-Specification-Version: 1.64 diff --git a/editor.util/src/org/netbeans/lib/editor/util/GapList.java b/editor.util/src/org/netbeans/lib/editor/util/GapList.java --- a/editor.util/src/org/netbeans/lib/editor/util/GapList.java +++ b/editor.util/src/org/netbeans/lib/editor/util/GapList.java @@ -122,6 +122,12 @@ this.gapLength = elementData.length - size; } + private GapList(E[] data, int gapStart, int gapLength) { + this.elementData = data; + this.gapStart = gapStart; + this.gapLength = gapLength; + } + /** * Trims the capacity of this GapList instance to be the * list's current size. An application can use this operation to minimize @@ -314,7 +320,19 @@ throw new InternalError(); } } - + + /** + * Create shallow copy of this gap list. + * @return copy of this gap list with zero extra capacity. + * @since 1.63 + */ + public GapList copy() { + int size = size(); + E[] data = allocateElementsArray(size); + copyAllData(data); + return new GapList(data, size, 0); + } + /** * @deprecated use {@link #copyElements(int, int, Object[], int)} which performs the same operation */ @@ -556,6 +574,28 @@ } /** + * Appends elements from the specified collection to the end of + * this list, in the order that they are returned by the + * specified collection's iterator. The behavior of this operation is + * undefined if the specified Collection is modified while the operation + * is in progress. (This implies that the behavior of this call is + * undefined if the specified Collection is this list, and this + * list is nonempty.) + * + * @param c collection containing the elements to be inserted into this list. + * @param off offset in the collection pointing to first element to copy. + * @param len number of elements to copy from the collection. + * @return true if this list changed as a result of the call. + * @throws IndexOutOfBoundsException if index out of range (index + * < 0 || index > size()). + * @throws NullPointerException if the specified Collection is null. + * @since 1.64 + */ + public boolean addAll(Collection c, int off, int len) { + return addArray(size(), c.toArray(), off, len); + } + + /** * Inserts all of the elements in the specified Collection into this * list, starting at the specified position. Shifts the element * currently at that position (if any) and any subsequent elements to @@ -575,12 +615,47 @@ return addArray(index, c.toArray()); } - /* + /** + * Inserts elements in the specified Collection into this + * list, starting at the specified position. Shifts the element + * currently at that position (if any) and any subsequent elements to + * the right (increases their indices). The new elements will appear + * in the list in the order that they are returned by the + * specified Collection's iterator. + * + * @param index index at which to insert first element + * from the specified collection. + * @param c collection containing the elements to be inserted into this list. + * @param off offset in the collection pointing to first element to copy. + * @param len number of elements to copy from the collection. + * @return true if this list changed as a result of the call. + * @throws IndexOutOfBoundsException if index out of range (index + * < 0 || index > size()). + * @throws NullPointerException if the specified Collection is null. + * @since 1.64 + */ + public boolean addAll(int index, Collection c, int off, int len) { + return addArray(index, c.toArray(), off, len); + } + + /** + * Inserts all elements of the given array at the end of this list. + * + * @param elements array of elements to insert. + * @return true if this list changed as a result of the call. + * @since 1.64 + */ + public boolean addArray(Object[] elements) { + return addArray(size(), elements, 0, elements.length); + } + + /** * Inserts all elements from the given array into this list, starting * at the given index. * * @param index index at which to insert first element from the array. * @param elements array of elements to insert. + * @return true if this list changed as a result of the call. */ public boolean addArray(int index, Object[] elements) { return addArray(index, elements, 0, elements.length); @@ -594,6 +669,7 @@ * @param elements array of elements from which to insert elements. * @param off offset in the elements pointing to first element to copy. * @param len number of elements to copy from the elements array. + * @return true if this list changed as a result of the call. */ public boolean addArray(int index, Object[] elements, int off, int len) { int size = size(); @@ -646,7 +722,7 @@ return oldValue; } - + /** * Removes elements at the given index. * diff --git a/editor.util/test/unit/src/org/netbeans/lib/editor/util/GapListRandomTest.java b/editor.util/test/unit/src/org/netbeans/lib/editor/util/GapListRandomTest.java --- a/editor.util/test/unit/src/org/netbeans/lib/editor/util/GapListRandomTest.java +++ b/editor.util/test/unit/src/org/netbeans/lib/editor/util/GapListRandomTest.java @@ -45,6 +45,7 @@ package org.netbeans.lib.editor.util; import java.util.ArrayList; +import java.util.Arrays; import java.util.Random; import junit.framework.TestCase; @@ -65,6 +66,7 @@ private static final int REMOVE_RANGE_RATIO_1 = 10; private static final int CLEAR_RATIO_1 = 5; private static final int SET_RATIO_1 = 50; + private static final int COPY_RATIO_1 = 50; private static final int OP_COUNT_2 = 10000; private static final int ADD_RATIO_2 = 50; @@ -74,6 +76,7 @@ private static final int REMOVE_RANGE_RATIO_2 = 10; private static final int CLEAR_RATIO_2 = 3; private static final int SET_RATIO_2 = 50; + private static final int COPY_RATIO_2 = 50; private ArrayList al; @@ -99,14 +102,14 @@ testRound(random, OP_COUNT_1, ADD_RATIO_1, ADD_ALL_RATIO_1, ADD_ALL_MAX_COUNT_1, - REMOVE_RATIO_1, REMOVE_RANGE_RATIO_1, CLEAR_RATIO_1, SET_RATIO_1); + REMOVE_RATIO_1, REMOVE_RANGE_RATIO_1, CLEAR_RATIO_1, SET_RATIO_1, COPY_RATIO_1); testRound(random, OP_COUNT_2, ADD_RATIO_2, ADD_ALL_RATIO_2, ADD_ALL_MAX_COUNT_2, - REMOVE_RATIO_2, REMOVE_RANGE_RATIO_2, CLEAR_RATIO_2, SET_RATIO_2); + REMOVE_RATIO_2, REMOVE_RANGE_RATIO_2, CLEAR_RATIO_2, SET_RATIO_2, COPY_RATIO_2); } private void testRound(Random random, int opCount, int addRatio, int addAllRatio, int addAllMaxCount, - int removeRatio, int removeRangeRatio, int clearRatio, int setRatio) { + int removeRatio, int removeRangeRatio, int clearRatio, int setRatio, int copyRatio) { int ratioSum = addRatio + addAllRatio + removeRatio + removeRangeRatio + clearRatio + setRatio; @@ -125,18 +128,55 @@ } else if ((r -= addAllRatio) < 0) { int count = (int)(random.nextDouble() * addAllMaxCount); + int index = (int)(random.nextDouble() * (al.size() + 1)); + int off = (int)(random.nextDouble() * count); + int len = (int)(random.nextDouble() * (count + 1 - off)); ArrayList l = new ArrayList(); for (int i = count; i > 0; i--) { l.add(new Object()); } + int methodType = (int)(random.nextDouble() * 5); + switch (methodType) { + case 0: // addAll(Collection c) + al.addAll(l); + if (debug) { + debugOp(op, "addAll(index, collection)"); // NOI18N + } + gl.addAll(l); + break; + case 1: // addAll(Collection c, int off, int len) + al.addAll(l.subList(off, off + len)); + if (debug) { + debugOp(op, "addAll(collection, off, len)"); // NOI18N + } + gl.addAll(l, off, len); + break; + case 2: // addAll(int index, Collection c) + al.addAll(index, l); + if (debug) { + debugOp(op, "addAll(index, collection)"); // NOI18N + } + gl.addAll(index, l); + break; + case 3: // addAll(int index, Collection c, int off, int len) + al.addAll(index, l.subList(off, off + len)); + if (debug) { + debugOp(op, "addAll(index, collection, off, len)"); // NOI18N + } + gl.addAll(index, l, off, len); + break; + case 4: // addArray(Object[] elements) + al.addAll(l); + if (debug) { + debugOp(op, "addArray(array)"); // NOI18N + } + gl.addArray(l.toArray()); + break; + + default: + throw new AssertionError(); + } - Object o = new Object(); - int index = (int)(al.size() * random.nextDouble()); - al.addAll(index, l); - if (debug) { - debugOp(op, "addAll() at index=" + index); // NOI18N - } - gl.addAll(index, l); } else if ((r -= removeRatio) < 0) { if (al.size() > 0) { // is anything to remove @@ -178,6 +218,29 @@ } gl.set(index, o); } + } else if ((r -= copyRatio) < 0) { + int methodType = (int)(random.nextDouble() * 3); + int alSize = al.size(); + int off = (int)(random.nextDouble() * alSize); + int len = (int)(random.nextDouble() * (alSize + 1 - off)); + switch (methodType) { + case 0: + GapList copyList = gl.copy(); + assertEquals("Lists differ", al, copyList); + break; + case 1: + ArrayList targetList = new ArrayList(); + gl.copyElements(off, len, targetList); + assertEquals("Lists differ", al.subList(off, off + len), targetList); + break; + case 2: + Object[] targetArray = new Object[len]; + gl.copyElements(off, len, targetArray, 0); + assertEquals("Lists differ", al.subList(off, off + len), Arrays.asList(targetArray)); + break; + default: + throw new AssertionError(); + } } checkConsistency(); diff --git a/editor.util/test/unit/src/org/netbeans/lib/editor/util/random/RandomTestContainer.java b/editor.util/test/unit/src/org/netbeans/lib/editor/util/random/RandomTestContainer.java --- a/editor.util/test/unit/src/org/netbeans/lib/editor/util/random/RandomTestContainer.java +++ b/editor.util/test/unit/src/org/netbeans/lib/editor/util/random/RandomTestContainer.java @@ -400,7 +400,7 @@ } context.incrementOpCount(); } - container.dumpString(container.name() + " finished successfully."); // NOI18N + container.dumpString(container.name() + " finished successfully.\n"); // NOI18N ok = true; } finally { if (!ok) { @@ -486,7 +486,7 @@ private StringBuilder logOpBuilder = new StringBuilder(256); - private int maxOpsLogged = 10; + private int maxOpsLogged = 4; // By default log only last 4 operations private final Map properties; @@ -600,11 +600,12 @@ logOpBuilder.setLength(0); addOpLog(opLog); } - if (!opSuccess) { // Dump the logs + if (true) { // Dump the op logs always (not only after failure) - might want to examine something StringBuilder sb = new StringBuilder(1024); - while (!opLogs.isEmpty()) { - sb.append(opLogs.removeFirst()); + for (String opLog : opLogs) { + sb.append(opLog); } + opLogs.clear(); container.dumpString(sb.toString()); } } diff --git a/editor/src/org/netbeans/modules/editor/resources/NetBeans-keybindings.xml b/editor/src/org/netbeans/modules/editor/resources/NetBeans-keybindings.xml --- a/editor/src/org/netbeans/modules/editor/resources/NetBeans-keybindings.xml +++ b/editor/src/org/netbeans/modules/editor/resources/NetBeans-keybindings.xml @@ -129,15 +129,24 @@ + + + + + + + + diff --git a/html.editor/src/org/netbeans/modules/html/editor/APIAccessor.java b/html.editor/src/org/netbeans/modules/html/editor/APIAccessor.java new file mode 100644 --- /dev/null +++ b/html.editor/src/org/netbeans/modules/html/editor/APIAccessor.java @@ -0,0 +1,63 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2016 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 2016 Sun Microsystems, Inc. + */ +package org.netbeans.modules.html.editor; + +import org.netbeans.modules.html.editor.api.HtmlKit; + +/** + * + * @author Ralph Ruijs + */ +public abstract class APIAccessor { + public static APIAccessor DEFAULT; + + static { + Class c = HtmlKit.class; + try { + Class.forName(c.getName(), true, c.getClassLoader()); + } catch (Exception ex) { + ex.printStackTrace(); + } + } + + public abstract void setContentType(HtmlKit kit, String contentType); +} diff --git a/html.editor/src/org/netbeans/modules/html/editor/HtmlTransferHandler.java b/html.editor/src/org/netbeans/modules/html/editor/HtmlTransferHandler.java --- a/html.editor/src/org/netbeans/modules/html/editor/HtmlTransferHandler.java +++ b/html.editor/src/org/netbeans/modules/html/editor/HtmlTransferHandler.java @@ -41,659 +41,161 @@ */ package org.netbeans.modules.html.editor; +import java.awt.datatransfer.Clipboard; import java.awt.datatransfer.DataFlavor; import java.awt.datatransfer.Transferable; -import java.awt.datatransfer.UnsupportedFlavorException; -import java.awt.im.InputContext; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.Reader; -import java.io.StringReader; -import java.io.StringWriter; +import java.awt.event.InputEvent; +import javax.swing.Icon; import javax.swing.JComponent; import javax.swing.JEditorPane; -import javax.swing.JPasswordField; import javax.swing.TransferHandler; import javax.swing.plaf.UIResource; -import javax.swing.text.BadLocationException; -import javax.swing.text.Document; import javax.swing.text.EditorKit; import javax.swing.text.JTextComponent; -import javax.swing.text.Position; +import org.netbeans.modules.html.editor.api.HtmlKit; /* !!!!!!!!!!!!!!!!!!!!! * - * classes bellow were taken from BasicTextUI and rewritten in the place marked - * with [REWRITE_PLACE]. This needs to be done to fix the issue #43309 + * fix for issue #43309 * * !!!!!!!!!!!!!!!!!!!!! */ public class HtmlTransferHandler extends TransferHandler implements UIResource { - private JTextComponent exportComp; - private boolean shouldRemove; - private int p0; - private int p1; + + public static void install(JTextComponent c) { + TransferHandler origHandler = c.getTransferHandler(); + if (!(origHandler instanceof HtmlTransferHandler)) { + c.setTransferHandler(new HtmlTransferHandler(c.getTransferHandler())); + } + } + + private final TransferHandler delegate; - /** - * Try to find a flavor that can be used to import a Transferable. The - * set of usable flavors are tried in the following order: - *
            - *
          1. First, an attempt is made to find a flavor matching the content - * type of the EditorKit for the component. - *
          2. Second, an attempt to find a text/plain flavor is made. - *
          3. Third, an attempt to find a flavor representing a String - * reference in the same VM is made. - *
          4. Lastly, DataFlavor.stringFlavor is searched for. - *
          - */ - protected DataFlavor getImportFlavor(DataFlavor[] flavors, JTextComponent c) { - DataFlavor plainFlavor = null; - DataFlavor refFlavor = null; - DataFlavor stringFlavor = null; - if (c instanceof JEditorPane) { - for (int i = 0; i < flavors.length; i++) { - String mime = flavors[i].getMimeType(); - if (mime.startsWith(((JEditorPane) c).getEditorKit().getContentType())) { - //return flavors[i]; [REWRITE_PLACE] - } else if (plainFlavor == null && mime.startsWith("text/plain")) { - //NOI18N - plainFlavor = flavors[i]; - } else if (refFlavor == null && mime.startsWith("application/x-java-jvm-local-objectref") && flavors[i].getRepresentationClass() == java.lang.String.class) { - refFlavor = flavors[i]; - } else if (stringFlavor == null && flavors[i].equals(DataFlavor.stringFlavor)) { - stringFlavor = flavors[i]; - } + public HtmlTransferHandler(TransferHandler delegate) { + this.delegate = delegate; + } + + TransferHandler getDelegate() { + return delegate; + } + + @Override + public boolean canImport(TransferSupport support) { + if(support.getComponent() instanceof JEditorPane && + ((JEditorPane) support.getComponent()).getEditorKit() instanceof HtmlKit) { + HtmlKit kit = (HtmlKit) ((JEditorPane) support.getComponent()).getEditorKit(); + APIAccessor.DEFAULT.setContentType(kit, "text/plain"); + try { + return delegate.canImport(support); + } finally { + APIAccessor.DEFAULT.setContentType(kit, HtmlKit.HTML_MIME_TYPE); } - if (plainFlavor != null) { - return plainFlavor; - } else if (refFlavor != null) { - return refFlavor; - } else if (stringFlavor != null) { - return stringFlavor; + } else { + return delegate.canImport(support); + } + } + + @Override + public boolean importData(TransferSupport support) { + if(support.getComponent() instanceof JEditorPane && + ((JEditorPane) support.getComponent()).getEditorKit() instanceof HtmlKit) { + HtmlKit kit = (HtmlKit) ((JEditorPane) support.getComponent()).getEditorKit(); + APIAccessor.DEFAULT.setContentType(kit, "text/plain"); + try { + return delegate.importData(support); + } finally { + APIAccessor.DEFAULT.setContentType(kit, HtmlKit.HTML_MIME_TYPE); } - return null; + } else { + return delegate.importData(support); } - for (int i = 0; i < flavors.length; i++) { - String mime = flavors[i].getMimeType(); - if (mime.startsWith("text/plain")) { - //NOI18N - return flavors[i]; - } else if (refFlavor == null && mime.startsWith("application/x-java-jvm-local-objectref") && flavors[i].getRepresentationClass() == java.lang.String.class) { - refFlavor = flavors[i]; - } else if (stringFlavor == null && flavors[i].equals(DataFlavor.stringFlavor)) { - stringFlavor = flavors[i]; + } + + @Override + public boolean canImport(JComponent comp, DataFlavor[] t) { + if(comp instanceof JEditorPane && + ((JEditorPane) comp).getEditorKit() instanceof HtmlKit) { + HtmlKit kit = (HtmlKit) ((JEditorPane) comp).getEditorKit(); + APIAccessor.DEFAULT.setContentType(kit, "text/plain"); + try { + return delegate.canImport(comp, t); + } finally { + APIAccessor.DEFAULT.setContentType(kit, HtmlKit.HTML_MIME_TYPE); } + } else { + return delegate.canImport(comp, t); } - if (refFlavor != null) { - return refFlavor; - } else if (stringFlavor != null) { - return stringFlavor; + } + + @Override + public boolean importData(JComponent comp, Transferable t) { + if(comp instanceof JEditorPane && + ((JEditorPane) comp).getEditorKit() instanceof HtmlKit) { + HtmlKit kit = (HtmlKit) ((JEditorPane) comp).getEditorKit(); + APIAccessor.DEFAULT.setContentType(kit, "text/plain"); + try { + return delegate.importData(comp, t); + } finally { + APIAccessor.DEFAULT.setContentType(kit, HtmlKit.HTML_MIME_TYPE); + } + } else { + return delegate.importData(comp, t); + } + } + + @Override + protected Transferable createTransferable(JComponent c) { + try { + java.lang.reflect.Method method = delegate.getClass().getDeclaredMethod( + "createTransferable", // NOI18N + new Class[]{javax.swing.JComponent.class}); + method.setAccessible(true); + + return (Transferable) method.invoke(delegate, new Object[]{c}); + } catch (NoSuchMethodException ex) { + ex.printStackTrace(); + } catch (IllegalAccessException ex) { + ex.printStackTrace(); + } catch (java.lang.reflect.InvocationTargetException ex) { + ex.printStackTrace(); } return null; } - /** - * Import the given stream data into the text component. - */ - protected void handleReaderImport(Reader in, JTextComponent c, boolean useRead) throws BadLocationException, IOException { - if (useRead) { - int startPosition = c.getSelectionStart(); - int endPosition = c.getSelectionEnd(); - int length = endPosition - startPosition; - EditorKit kit = c.getUI().getEditorKit(c); - Document doc = c.getDocument(); - if (length > 0) { - doc.remove(startPosition, length); - } - kit.read(in, doc, startPosition); - } else { - char[] buff = new char[1024]; - int nch; - boolean lastWasCR = false; - int last; - StringBuffer sbuff = null; - // Read in a block at a time, mapping \r\n to \n, as well as single - // \r to \n. - while ((nch = in.read(buff, 0, buff.length)) != -1) { - if (sbuff == null) { - sbuff = new StringBuffer(nch); - } - last = 0; - for (int counter = 0; counter < nch; counter++) { - switch (buff[counter]) { - case '\r': - if (lastWasCR) { - if (counter == 0) { - sbuff.append('\n'); - } else { - buff[counter - 1] = '\n'; - } - } else { - lastWasCR = true; - } - break; - case '\n': - if (lastWasCR) { - if (counter > (last + 1)) { - sbuff.append(buff, last, counter - last - 1); - } - // else nothing to do, can skip \r, next write will - // write \n - lastWasCR = false; - last = counter; - } - break; - default: - if (lastWasCR) { - if (counter == 0) { - sbuff.append('\n'); - } else { - buff[counter - 1] = '\n'; - } - lastWasCR = false; - } - break; - } - } - if (last < nch) { - if (lastWasCR) { - if (last < (nch - 1)) { - sbuff.append(buff, last, nch - last - 1); - } - } else { - sbuff.append(buff, last, nch - last); - } - } - } - if (lastWasCR) { - sbuff.append('\n'); - } - c.replaceSelection(sbuff != null ? sbuff.toString() : ""); //NOI18N + @Override + public void exportAsDrag(JComponent comp, InputEvent e, int action) { + delegate.exportAsDrag(comp, e, action); + } + + @Override + protected void exportDone(JComponent source, Transferable data, int action) { + try { + java.lang.reflect.Method method = delegate.getClass().getDeclaredMethod( + "exportDone", // NOI18N + new Class[]{javax.swing.JComponent.class, Transferable.class, int.class}); + method.setAccessible(true); + method.invoke(delegate, new Object[]{source, data, new Integer(action)}); + } catch (NoSuchMethodException ex) { + ex.printStackTrace(); + } catch (IllegalAccessException ex) { + ex.printStackTrace(); + } catch (java.lang.reflect.InvocationTargetException ex) { + ex.printStackTrace(); } } - // --- TransferHandler methods ------------------------------------ - /** - * This is the type of transfer actions supported by the source. Some - * models are not mutable, so a transfer operation of COPY only should - * be advertised in that case. - * - * @param c The component holding the data to be transfered. This - * argument is provided to enable sharing of TransferHandlers by - * multiple components. - * @return This is implemented to return NONE if the component is a - * JPasswordField since exporting data via user gestures is not allowed. - * If the text component is editable, COPY_OR_MOVE is returned, - * otherwise just COPY is allowed. - */ + @Override + public void exportToClipboard(JComponent c, Clipboard clip, int action) throws IllegalStateException { + delegate.exportToClipboard(c, clip, action); + } + @Override public int getSourceActions(JComponent c) { - int actions = NONE; - if (!(c instanceof JPasswordField)) { - if (((JTextComponent) c).isEditable()) { - actions = COPY_OR_MOVE; - } else { - actions = COPY; - } - } - return actions; + return delegate.getSourceActions(c); } - /** - * Create a Transferable to use as the source for a data transfer. - * - * @param comp The component holding the data to be transfered. This - * argument is provided to enable sharing of TransferHandlers by - * multiple components. - * @return The representation of the data to be transfered. - * - */ @Override - protected Transferable createTransferable(JComponent comp) { - exportComp = (JTextComponent) comp; - shouldRemove = true; - p0 = exportComp.getSelectionStart(); - p1 = exportComp.getSelectionEnd(); - return (p0 != p1) ? (new HtmlTransferable(exportComp, p0, p1)) : null; + public Icon getVisualRepresentation(Transferable t) { + return delegate.getVisualRepresentation(t); } - - /** - * This method is called after data has been exported. This method - * should remove the data that was transfered if the action was MOVE. - * - * @param source The component that was the source of the data. - * @param data The data that was transferred or possibly null if the - * action is NONE. - * @param action The actual action that was performed. - */ - @Override - protected void exportDone(JComponent source, Transferable data, int action) { - // only remove the text if shouldRemove has not been set to - // false by importData and only if the action is a move - if (shouldRemove && action == MOVE) { - HtmlTransferable t = (HtmlTransferable) data; - t.removeText(); - } - exportComp = null; - } - - /** - * This method causes a transfer to a component from a clipboard or a - * DND drop operation. The Transferable represents the data to be - * imported into the component. - * - * @param comp The component to receive the transfer. This argument is - * provided to enable sharing of TransferHandlers by multiple - * components. - * @param t The data to import - * @return true if the data was inserted into the component, false - * otherwise. - */ - @Override - public boolean importData(JComponent comp, Transferable t) { - JTextComponent c = (JTextComponent) comp; - // if we are importing to the same component that we exported from - // then don't actually do anything if the drop location is inside - // the drag location and set shouldRemove to false so that exportDone - // knows not to remove any data - if (c == exportComp && c.getCaretPosition() >= p0 && c.getCaretPosition() <= p1) { - shouldRemove = false; - return true; - } - boolean imported = false; - DataFlavor importFlavor = getImportFlavor(t.getTransferDataFlavors(), c); - if (importFlavor != null) { - try { - boolean useRead = false; - if (comp instanceof JEditorPane) { - JEditorPane ep = (JEditorPane) comp; - if (!ep.getContentType().startsWith("text/plain") && importFlavor.getMimeType().startsWith(ep.getContentType())) { - useRead = true; - } - } - InputContext ic = c.getInputContext(); - if (ic != null) { - ic.endComposition(); - } - Reader r = importFlavor.getReaderForText(t); - handleReaderImport(r, c, useRead); - imported = true; - } catch (UnsupportedFlavorException | BadLocationException | IOException ufe) { - //just ignore - } - } - return imported; - } - - /** - * This method indicates if a component would accept an import of the - * given set of data flavors prior to actually attempting to import it. - * - * @param comp The component to receive the transfer. This argument is - * provided to enable sharing of TransferHandlers by multiple - * components. - * @param flavors The data formats available - * @return true if the data can be inserted into the component, false - * otherwise. - */ - @Override - public boolean canImport(JComponent comp, DataFlavor[] flavors) { - JTextComponent c = (JTextComponent) comp; - if (!(c.isEditable() && c.isEnabled())) { - return false; - } - return getImportFlavor(flavors, c) != null; - } - - /** - * A possible implementation of the Transferable interface for text - * components. For a JEditorPane with a rich set of EditorKit - * implementations, conversions could be made giving a wider set of - * formats. This is implemented to offer up only the active content type - * and text/plain (if that is not the active format) since that can be - * extracted from other formats. - */ - static class HtmlTransferable extends BasicTransferable { - - HtmlTransferable(JTextComponent c, int start, int end) { - super(null, null); - this.c = c; - Document doc = c.getDocument(); - try { - p0 = doc.createPosition(start); - p1 = doc.createPosition(end); - plainData = c.getSelectedText(); - if (c instanceof JEditorPane) { - JEditorPane ep = (JEditorPane) c; - mimeType = ep.getContentType(); - if (mimeType.startsWith("text/plain")) { - //NOI18N - return; - } - StringWriter sw = new StringWriter(p1.getOffset() - p0.getOffset()); - ep.getEditorKit().write(sw, doc, p0.getOffset(), p1.getOffset() - p0.getOffset()); - if (mimeType.startsWith("text/html")) { - //NOI18N - htmlData = sw.toString(); - } else { - richText = sw.toString(); - } - } - } catch (BadLocationException | IOException ble) { - } - } - - void removeText() { - if ((p0 != null) && (p1 != null) && (p0.getOffset() != p1.getOffset())) { - try { - Document doc = c.getDocument(); - doc.remove(p0.getOffset(), p1.getOffset() - p0.getOffset()); - } catch (BadLocationException e) { - } - } - } - - // ---- EditorKit other than plain or HTML text ----------------------- - /** - * If the EditorKit is not for text/plain or text/html, that format - * is supported through the "richer flavors" part of - * BasicTransferable. - */ - @Override - protected DataFlavor[] getRicherFlavors() { - if (richText == null) { - return null; - } - try { - DataFlavor[] flavors = new DataFlavor[3]; - flavors[0] = new DataFlavor(mimeType + ";class=java.lang.String"); //NOI18N - flavors[1] = new DataFlavor(mimeType + ";class=java.io.Reader"); //NOI18N - flavors[2] = new DataFlavor(mimeType + ";class=java.io.InputStream;charset=unicode"); //NOI18N - return flavors; - } catch (ClassNotFoundException cle) { - // fall through to unsupported (should not happen) - } - return null; - } - - /** - * The only richer format supported is the file list flavor - */ - @Override - protected Object getRicherData(DataFlavor flavor) throws UnsupportedFlavorException { - if (richText == null) { - return null; - } - if (String.class.equals(flavor.getRepresentationClass())) { - return richText; - } else if (Reader.class.equals(flavor.getRepresentationClass())) { - return new StringReader(richText); - } else if (InputStream.class.equals(flavor.getRepresentationClass())) { - return new ByteArrayInputStream(richText.getBytes()); - } - throw new UnsupportedFlavorException(flavor); - } - Position p0; - Position p1; - String mimeType; - String richText; - JTextComponent c; - } - - private static class BasicTransferable implements Transferable, UIResource { - - protected String plainData; - protected String htmlData; - private static DataFlavor[] htmlFlavors; - private static DataFlavor[] stringFlavors; - private static DataFlavor[] plainFlavors; - - static { - try { - htmlFlavors = new DataFlavor[3]; - htmlFlavors[0] = new DataFlavor("text/html;class=java.lang.String"); //NOI18N - htmlFlavors[1] = new DataFlavor("text/html;class=java.io.Reader"); //NOI18N - htmlFlavors[2] = new DataFlavor("text/html;charset=unicode;class=java.io.InputStream"); //NOI18N - - plainFlavors = new DataFlavor[3]; - plainFlavors[0] = new DataFlavor("text/plain;class=java.lang.String"); //NOI18N - plainFlavors[1] = new DataFlavor("text/plain;class=java.io.Reader"); //NOI18N - plainFlavors[2] = new DataFlavor("text/plain;charset=unicode;class=java.io.InputStream"); //NOI18N - - stringFlavors = new DataFlavor[2]; - stringFlavors[0] = new DataFlavor(DataFlavor.javaJVMLocalObjectMimeType + ";class=java.lang.String"); //NOI18N - stringFlavors[1] = DataFlavor.stringFlavor; - - } catch (ClassNotFoundException cle) { - System.err.println("error initializing javax.swing.plaf.basic.BasicTranserable"); ////NOI18N - } - } - - public BasicTransferable(String plainData, String htmlData) { - this.plainData = plainData; - this.htmlData = htmlData; - } - - /** - * Returns an array of DataFlavor objects indicating the flavors the - * data can be provided in. The array should be ordered according to - * preference for providing the data (from most richly descriptive to - * least descriptive). - * - * @return an array of data flavors in which this data can be - * transferred - */ - @Override - public DataFlavor[] getTransferDataFlavors() { - DataFlavor[] richerFlavors = getRicherFlavors(); - int nRicher = (richerFlavors != null) ? richerFlavors.length : 0; - int nHTML = (isHTMLSupported()) ? htmlFlavors.length : 0; - int nPlain = (isPlainSupported()) ? plainFlavors.length : 0; - int nString = (isPlainSupported()) ? stringFlavors.length : 0; - int nFlavors = nRicher + nHTML + nPlain + nString; - DataFlavor[] flavors = new DataFlavor[nFlavors]; - - // fill in the array - int nDone = 0; - if (nRicher > 0) { - System.arraycopy(richerFlavors, 0, flavors, nDone, nRicher); - nDone += nRicher; - } - if (nHTML > 0) { - System.arraycopy(htmlFlavors, 0, flavors, nDone, nHTML); - nDone += nHTML; - } - if (nPlain > 0) { - System.arraycopy(plainFlavors, 0, flavors, nDone, nPlain); - nDone += nPlain; - } - if (nString > 0) { - System.arraycopy(stringFlavors, 0, flavors, nDone, nString); - nDone += nString; - } - return flavors; - } - - /** - * Returns whether or not the specified data flavor is supported for - * this object. - * - * @param flavor the requested flavor for the data - * @return boolean indicating whether or not the data flavor is - * supported - */ - @Override - public boolean isDataFlavorSupported(DataFlavor flavor) { - DataFlavor[] flavors = getTransferDataFlavors(); - for (int i = 0; i < flavors.length; i++) { - if (flavors[i].equals(flavor)) { - return true; - } - } - return false; - } - - /** - * Returns an object which represents the data to be transferred. The - * class of the object returned is defined by the representation class - * of the flavor. - * - * @param flavor the requested flavor for the data - * @see DataFlavor#getRepresentationClass - * @exception IOException if the data is no longer available in the - * requested flavor. - * @exception UnsupportedFlavorException if the requested data flavor is - * not supported. - */ - @Override - public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException { - DataFlavor[] richerFlavors = getRicherFlavors(); - if (isRicherFlavor(flavor)) { - return getRicherData(flavor); - } else if (isHTMLFlavor(flavor)) { - String data = getHTMLData(); - data = (data == null) ? "" : data; //NOI18N - if (String.class.equals(flavor.getRepresentationClass())) { - return data; - } else if (Reader.class.equals(flavor.getRepresentationClass())) { - return new StringReader(data); - } else if (InputStream.class.equals(flavor.getRepresentationClass())) { - return new ByteArrayInputStream(data.getBytes()); - } - // fall through to unsupported - } else if (isPlainFlavor(flavor)) { - String data = getPlainData(); - data = (data == null) ? "" : data; - if (String.class.equals(flavor.getRepresentationClass())) { - return data; - } else if (Reader.class.equals(flavor.getRepresentationClass())) { - return new StringReader(data); - } else if (InputStream.class.equals(flavor.getRepresentationClass())) { - return new ByteArrayInputStream(data.getBytes()); - } - // fall through to unsupported - - } else if (isStringFlavor(flavor)) { - String data = getPlainData(); - data = (data == null) ? "" : data; //NOI18N - return data; - } - throw new UnsupportedFlavorException(flavor); - } - - // --- richer subclass flavors ---------------------------------------------- - protected boolean isRicherFlavor(DataFlavor flavor) { - DataFlavor[] richerFlavors = getRicherFlavors(); - int nFlavors = (richerFlavors != null) ? richerFlavors.length : 0; - for (int i = 0; i < nFlavors; i++) { - if (richerFlavors[i].equals(flavor)) { - return true; - } - } - return false; - } - - /** - * Some subclasses will have flavors that are more descriptive than HTML - * or plain text. If this method returns a non-null value, it will be - * placed at the start of the array of supported flavors. - */ - protected DataFlavor[] getRicherFlavors() { - return null; - } - - protected Object getRicherData(DataFlavor flavor) throws UnsupportedFlavorException { - return null; - } - - // --- html flavors ---------------------------------------------------------- - /** - * Returns whether or not the specified data flavor is an HTML flavor - * that is supported. - * - * @param flavor the requested flavor for the data - * @return boolean indicating whether or not the data flavor is - * supported - */ - protected boolean isHTMLFlavor(DataFlavor flavor) { - DataFlavor[] flavors = htmlFlavors; - for (int i = 0; i < flavors.length; i++) { - if (flavors[i].equals(flavor)) { - return true; - } - } - return false; - } - - /** - * Should the HTML flavors be offered? If so, the method getHTMLData - * should be implemented to provide something reasonable. - */ - protected boolean isHTMLSupported() { - return htmlData != null; - } - - /** - * Fetch the data in a text/html format - */ - protected String getHTMLData() { - return htmlData; - } - - // --- plain text flavors ---------------------------------------------------- - /** - * Returns whether or not the specified data flavor is an plain flavor - * that is supported. - * - * @param flavor the requested flavor for the data - * @return boolean indicating whether or not the data flavor is - * supported - */ - protected boolean isPlainFlavor(DataFlavor flavor) { - DataFlavor[] flavors = plainFlavors; - for (int i = 0; i < flavors.length; i++) { - if (flavors[i].equals(flavor)) { - return true; - } - } - return false; - } - - /** - * Should the plain text flavors be offered? If so, the method - * getPlainData should be implemented to provide something reasonable. - */ - protected boolean isPlainSupported() { - return plainData != null; - } - - /** - * Fetch the data in a text/plain format. - */ - protected String getPlainData() { - return plainData; - } - - // --- string flavorss -------------------------------------------------------- - /** - * Returns whether or not the specified data flavor is a String flavor - * that is supported. - * - * @param flavor the requested flavor for the data - * @return boolean indicating whether or not the data flavor is - * supported - */ - protected boolean isStringFlavor(DataFlavor flavor) { - DataFlavor[] flavors = stringFlavors; - for (int i = 0; i < flavors.length; i++) { - if (flavors[i].equals(flavor)) { - return true; - } - } - return false; - } - } - // END of fix of issue #43309 - } diff --git a/html.editor/src/org/netbeans/modules/html/editor/api/AccessorImpl.java b/html.editor/src/org/netbeans/modules/html/editor/api/AccessorImpl.java new file mode 100644 --- /dev/null +++ b/html.editor/src/org/netbeans/modules/html/editor/api/AccessorImpl.java @@ -0,0 +1,58 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2016 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 2016 Sun Microsystems, Inc. + */ +package org.netbeans.modules.html.editor.api; + +import org.netbeans.modules.html.editor.APIAccessor; + +/** + * + * @author Ralph Ruijs + */ +class AccessorImpl extends APIAccessor { + + AccessorImpl() {} + + @Override + public void setContentType(HtmlKit kit, String contentType) { + kit.setContentType(contentType); + } +} diff --git a/html.editor/src/org/netbeans/modules/html/editor/api/HtmlKit.java b/html.editor/src/org/netbeans/modules/html/editor/api/HtmlKit.java --- a/html.editor/src/org/netbeans/modules/html/editor/api/HtmlKit.java +++ b/html.editor/src/org/netbeans/modules/html/editor/api/HtmlKit.java @@ -49,6 +49,7 @@ import org.netbeans.editor.ext.ExtKit; import org.netbeans.modules.csl.api.CslActions; import org.netbeans.modules.editor.NbEditorKit; +import org.netbeans.modules.html.editor.APIAccessor; /** * Editor kit implementation for HTML content type @@ -57,6 +58,10 @@ * */ public class HtmlKit extends NbEditorKit implements org.openide.util.HelpCtx.Provider { + + static { + APIAccessor.DEFAULT = new AccessorImpl(); + } public @Override org.openide.util.HelpCtx getHelpCtx() { @@ -65,6 +70,7 @@ static final long serialVersionUID = -1381945567613910297L; public static final String HTML_MIME_TYPE = "text/html"; // NOI18N + private String contentType; public HtmlKit() { this(HTML_MIME_TYPE); @@ -72,11 +78,16 @@ public HtmlKit(String mimeType) { super(); + contentType = HTML_MIME_TYPE; } @Override public String getContentType() { - return HTML_MIME_TYPE; + return contentType; + } + + void setContentType(String contentType) { + this.contentType = contentType; } @Override @@ -90,7 +101,7 @@ @Override public void install(javax.swing.JEditorPane c) { super.install(c); - c.setTransferHandler(new HtmlTransferHandler()); + HtmlTransferHandler.install(c); } @Override diff --git a/java.editor/src/org/netbeans/modules/java/editor/imports/ClipboardHandler.java b/java.editor/src/org/netbeans/modules/java/editor/imports/ClipboardHandler.java --- a/java.editor/src/org/netbeans/modules/java/editor/imports/ClipboardHandler.java +++ b/java.editor/src/org/netbeans/modules/java/editor/imports/ClipboardHandler.java @@ -97,6 +97,7 @@ import com.sun.source.tree.ClassTree; import com.sun.source.tree.ImportTree; import com.sun.source.tree.MethodTree; +import java.awt.Component; import java.util.Set; import org.netbeans.api.editor.EditorActionRegistration; import org.netbeans.api.editor.EditorActionRegistrations; @@ -550,12 +551,14 @@ } @Override - public boolean importData(TransferSupport support) { - return delegate.importData(support); + public boolean importData(JComponent comp, Transferable t) { + return delegate.importData(comp, t); } @Override - public boolean importData(JComponent comp, Transferable t) { + public boolean importData(TransferSupport support) { + Transferable t = support.getTransferable(); + Component comp = support.getComponent(); if (t.isDataFlavorSupported(IMPORT_FLAVOR) && comp instanceof JTextComponent && !insideToken((JTextComponent)comp, JavaTokenId.STRING_LITERAL, JavaTokenId.BLOCK_COMMENT, JavaTokenId.JAVADOC_COMMENT, JavaTokenId.LINE_COMMENT)) { boolean result = false; @@ -563,7 +566,7 @@ final JTextComponent tc = (JTextComponent) comp; final int caret = tc.getSelectionStart(); - if (result = delegatedImportData(comp, t)) { + if (result = delegatedImportData(support)) { final ImportsWrapper imports = (ImportsWrapper) t.getTransferData(IMPORT_FLAVOR); final FileObject file = NbEditorUtilities.getFileObject(tc.getDocument()); final Document doc = tc.getDocument(); @@ -610,11 +613,13 @@ return result; } - return delegatedImportData(comp, t); + return delegatedImportData(support); } - private boolean delegatedImportData(final JComponent comp, final Transferable t) { - if (comp instanceof JTextComponent && !t.isDataFlavorSupported(COPY_FROM_STRING_FLAVOR) && insideToken((JTextComponent) comp, JavaTokenId.STRING_LITERAL)) { + private boolean delegatedImportData(final TransferSupport support) { + JComponent comp = (JComponent) support.getComponent(); + if (comp instanceof JTextComponent && !support.isDataFlavorSupported(COPY_FROM_STRING_FLAVOR) && insideToken((JTextComponent) comp, JavaTokenId.STRING_LITERAL)) { + final Transferable t = support.getTransferable(); return delegate.importData(comp, new Transferable() { @Override public DataFlavor[] getTransferDataFlavors() { @@ -654,7 +659,7 @@ } }); } - return delegate.importData(comp, t); + return delegate.importData(support); } private boolean insideToken(final JTextComponent jtc, final JavaTokenId first, final JavaTokenId... rest) { diff --git a/openide.text/src/org/openide/text/QuietEditorPane.java b/openide.text/src/org/openide/text/QuietEditorPane.java --- a/openide.text/src/org/openide/text/QuietEditorPane.java +++ b/openide.text/src/org/openide/text/QuietEditorPane.java @@ -373,9 +373,15 @@ @Override public boolean importData(JComponent comp, Transferable t) { + return delegator.importData(comp, t); + } + + @Override + public boolean importData(TransferSupport t) { try { if (t.isDataFlavorSupported(ActiveEditorDrop.FLAVOR)){ - Object obj = t.getTransferData(ActiveEditorDrop.FLAVOR); + Object obj = t.getTransferable().getTransferData(ActiveEditorDrop.FLAVOR); + final JComponent comp = (JComponent) t.getComponent(); if (obj instanceof ActiveEditorDrop && comp instanceof JTextComponent){ boolean success = false; try { @@ -390,7 +396,7 @@ } catch (Exception exc){ exc.printStackTrace(); } - return delegator.importData(comp, t); + return delegator.importData(t); } private void requestFocus(JComponent comp) { @@ -412,15 +418,21 @@ comp.requestFocus(); } } + + @Override + public boolean canImport(JComponent comp, DataFlavor[] transferFlavors) { + return delegator.canImport(comp, transferFlavors); + } @Override - public boolean canImport(JComponent comp, DataFlavor[] transferFlavors) { + public boolean canImport(TransferSupport support) { + DataFlavor[] transferFlavors = support.getDataFlavors(); for (int i=0; i - + +