correct navigation in bidirectional text using keyboard arrow keys (with caret stopping and switching direction on ltr/rtl boundary)

This commit is contained in:
Dmitry Batrak
2015-05-28 18:41:15 +03:00
parent 961ee6f337
commit d97ae9d858
10 changed files with 108 additions and 8 deletions
@@ -302,4 +302,11 @@ public interface Caret extends UserDataHolderEx, Disposable {
* to offset and logical column number in the vicinity of caret.
*/
boolean isAtRtlLocation();
/**
* Returns <code>true</code> if caret is located at a boundary between LTR and RTL text fragments. Caret can located at any side of the
* boundary, exact location can be determined from directionality flags of caret's logical and visual position
* ({@link LogicalPosition#leansForward} and {@link VisualPosition#leansRight}).
*/
boolean isAtDirectionBoundary();
}
@@ -77,6 +77,13 @@ public class VisualPosition {
return line > other.line;
}
/**
* Constructs a new <code>VisualPosition</code> instance with a given value of {@link #leansRight} flag.
*/
public VisualPosition leanRight(boolean value) {
return new VisualPosition(line, column, value);
}
@NonNls
public String toString() {
return "VisualPosition: (" + line + ", " + column+")" + (leansRight ? " leans right" : "");
@@ -242,4 +242,9 @@ public class InjectedCaret implements Caret {
public boolean isAtRtlLocation() {
return myDelegate.isAtRtlLocation();
}
@Override
public boolean isAtDirectionBoundary() {
return myDelegate.isAtDirectionBoundary();
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2000-2014 JetBrains s.r.o.
* Copyright 2000-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -69,8 +69,14 @@ class MoveCaretLeftOrRightHandler extends EditorActionHandler {
}
}
}
final boolean scrollToCaret = (!(editor instanceof EditorImpl) || ((EditorImpl)editor).isScrollToCaret())
&& caret == editor.getCaretModel().getPrimaryCaret();
caretModel.moveCaretRelatively(myDirection == Direction.RIGHT ? 1 : -1, 0, false, false, scrollToCaret);
VisualPosition currentPosition = caret.getVisualPosition();
if (caret.isAtDirectionBoundary() && (myDirection == Direction.RIGHT ^ currentPosition.leansRight)) {
caret.moveToVisualPosition(currentPosition.leanRight(!currentPosition.leansRight));
}
else {
final boolean scrollToCaret = (!(editor instanceof EditorImpl) || ((EditorImpl)editor).isScrollToCaret())
&& caret == editor.getCaretModel().getPrimaryCaret();
caretModel.moveCaretRelatively(myDirection == Direction.RIGHT ? 1 : -1, 0, false, false, scrollToCaret);
}
}
}
@@ -267,6 +267,8 @@ public class CaretImpl extends UserDataHolderBase implements Caret {
int newLineNumber = visualCaret.line + lineShift;
int newColumnNumber = visualCaret.column + columnShift;
boolean newLeansRight = lineShift == 0 && columnShift != 0 ? columnShift < 0 : visualCaret.leansRight;
if (desiredX >= 0) {
newColumnNumber = myEditor.xyToVisualPosition(new Point(desiredX, Math.max(0, newLineNumber) * myEditor.getLineHeight())).column;
}
@@ -305,13 +307,14 @@ public class CaretImpl extends UserDataHolderBase implements Caret {
VisualPosition pos = new VisualPosition(newLineNumber, newColumnNumber);
if (!myEditor.getSoftWrapModel().isInsideSoftWrap(pos)) {
LogicalPosition log = myEditor.visualToLogicalPosition(new VisualPosition(newLineNumber, newColumnNumber));
LogicalPosition log = myEditor.visualToLogicalPosition(new VisualPosition(newLineNumber, newColumnNumber, newLeansRight));
int offset = myEditor.logicalPositionToOffset(log);
if (offset >= document.getTextLength()) {
int lastOffsetColumn = myEditor.offsetToVisualPosition(document.getTextLength()).column;
int lastOffsetColumn = myEditor.offsetToVisualPosition(document.getTextLength(), true).column;
// We want to move caret to the last column if if it's located at the last line and 'Down' is pressed.
if (lastOffsetColumn > newColumnNumber) {
newColumnNumber = lastOffsetColumn;
newLeansRight = true;
desiredX = -1;
lastColumnNumber = -1;
}
@@ -321,7 +324,7 @@ public class CaretImpl extends UserDataHolderBase implements Caret {
if (offset >= 0 && offset < document.getTextLength()) {
if (text.charAt(offset) == '\t' && (columnShift <= 0 || offset == myOffset)) {
if (columnShift <= 0) {
newColumnNumber = myEditor.offsetToVisualPosition(offset).column;
newColumnNumber = myEditor.offsetToVisualPosition(offset, true).column;
}
else {
SoftWrap softWrap = myEditor.getSoftWrapModel().getSoftWrap(offset + 1);
@@ -339,7 +342,7 @@ public class CaretImpl extends UserDataHolderBase implements Caret {
}
}
pos = new VisualPosition(newLineNumber, newColumnNumber);
pos = new VisualPosition(newLineNumber, newColumnNumber, newLeansRight);
if (columnShift != 0 && lineShift == 0 && myEditor.getSoftWrapModel().isInsideSoftWrap(pos)) {
LogicalPosition logical = myEditor.visualToLogicalPosition(pos);
int softWrapOffset = myEditor.logicalPositionToOffset(logical);
@@ -1466,6 +1469,11 @@ public class CaretImpl extends UserDataHolderBase implements Caret {
return myEditor.myUseNewRendering && myEditor.myView.isRtlLocation(myOffset, myLogicalCaret.leansForward);
}
@Override
public boolean isAtDirectionBoundary() {
return myEditor.myUseNewRendering && myEditor.myView.isDirectionBoundary(myOffset);
}
/**
* Encapsulates information about target vertical range info - its <code>'y'</code> coordinate and height in pixels.
*/
@@ -268,6 +268,14 @@ public class EditorView implements Disposable {
return layout.isRtlLocation(offset - myDocument.getLineStartOffset(line), leanForward);
}
public boolean isDirectionBoundary(int offset) {
assertIsDispatchThread();
if (myDocument.getTextLength() == 0) return false;
int line = myDocument.getLineNumber(offset);
LineLayout layout = getLineLayout(line);
return layout.isDirectionBoundary(offset - myDocument.getLineStartOffset(line));
}
@NotNull
LineLayout getLineLayout(int line) {
return myTextLayoutCache.getLineLayout(line);
@@ -212,6 +212,17 @@ class LineLayout {
return false;
}
boolean isDirectionBoundary(int offset) {
boolean prevIsRtl = false;
for (BidiRun run : myBidiRunsInLogicalOrder) {
boolean curIsRtl = run.isRtl();
if (offset == run.startOffset && curIsRtl != prevIsRtl) return true;
if (offset < run.endOffset) return false;
prevIsRtl = curIsRtl;
}
return prevIsRtl;
}
private static class BidiRun {
private final byte level;
private final int startOffset;
@@ -203,6 +203,11 @@ public class TextComponentCaret extends UserDataHolderBase implements Caret {
return false;
}
@Override
public boolean isAtDirectionBoundary() {
return false;
}
private SelectionModel getSelectionModel() {
return myEditor.getSelectionModel();
}
@@ -235,6 +235,29 @@ public class EditorRtlTest extends AbstractEditorTest {
assertLogicalPositionsEqual("Wrong logical position", lF(1), myEditor.getCaretModel().getLogicalPosition());
}
public void testNavigationWithArrowKeys() throws Exception {
init("llrrll\nllrrll");
assertCaretPosition(vL(0));
right();
assertCaretPosition(vL(1));
right();
assertCaretPosition(vL(2));
right();
assertCaretPosition(vR(2));
right();
assertCaretPosition(vL(3));
right();
assertCaretPosition(vL(4));
down();
assertCaretPosition(v(1, 4, false));
left();
assertCaretPosition(v(1, 3, true));
left();
assertCaretPosition(v(1, 2, true));
up();
assertCaretPosition(vR(2));
}
private void init(String text) throws IOException {
initText(text.replace(RTL_CHAR_REPRESENTATION, RTL_CHAR));
}
@@ -299,6 +322,10 @@ public class EditorRtlTest extends AbstractEditorTest {
assertEquals(message, expectedPosition, actualPosition);
assertEquals(message + " (direction flag)", expectedPosition.leansRight, actualPosition.leansRight);
}
private static void assertCaretPosition(VisualPosition visualPosition) {
assertVisualPositionsEqual("Wrong caret position", visualPosition, myEditor.getCaretModel().getVisualPosition());
}
// logical position leaning backward
private static LogicalPosition lB(int column) {
@@ -574,6 +574,22 @@ public abstract class LightPlatformCodeInsightTestCase extends LightPlatformTest
executeAction("EditorSelectLine");
}
protected static void left() {
executeAction("EditorLeft");
}
protected static void right() {
executeAction("EditorRight");
}
protected static void up() {
executeAction("EditorUp");
}
protected static void down() {
executeAction("EditorDown");
}
protected static void lineComment() {
new CommentByLineCommentAction().actionPerformedImpl(getProject(), getEditor());
}