Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CodenameOne/src/com/codename1/ui/TextInputClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,16 @@ interface CompositionCancellation {
/// editor with no indentation behavior may treat it as inserting a tab character.
int KEY_TAB = 18;

/// Key command: scroll up one viewport without moving the caret or selection.
int KEY_SCROLL_PAGE_UP = 19;
/// Key command: scroll down one viewport without moving the caret or selection.
int KEY_SCROLL_PAGE_DOWN = 20;

/// Key command: move to the next word boundary spatially to the left, including in RTL text.
int KEY_WORD_LEFT = 21;
/// Key command: move to the next word boundary spatially to the right, including in RTL text.
int KEY_WORD_RIGHT = 22;

/// Modifier bit: shift held (used to extend a selection).
int MOD_SHIFT = 1;
/// Modifier bit: control or command held.
Expand Down
6 changes: 3 additions & 3 deletions CodenameOne/src/com/codename1/ui/TextInputGeometry.java
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ public static TextInputGeometry capture(TextInputClient client) {
return ((Provider) client).getGeometrySnapshot();
}
TextInputState state = client.getEditingState();
int caret = state.getSelectionEnd();
int caret = state.getSelectionFocus();
Comment thread
shai-almog marked this conversation as resolved.
int first = caret;
int last = caret;
if (client instanceof Component) {
Expand Down Expand Up @@ -100,8 +100,8 @@ public static TextInputGeometry capture(TextInputClient client) {
points[n++] = rect[j];
}
}
return new TextInputGeometry(points, state.getSelectionStart(), caret,
client.selectionRects(state.getSelectionStart(), caret), client.getCaretRect());
return new TextInputGeometry(points, state.getSelectionAnchor(), caret,
client.selectionRects(state.getSelectionStart(), state.getSelectionEnd()), client.getCaretRect());
}

/// Returns the last captured caret rectangle for an offset, or the active caret offscreen.
Expand Down
28 changes: 19 additions & 9 deletions CodenameOne/src/com/codename1/ui/TextInputState.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@
/// IME composition is active.
public final class TextInputState {
private final String text;
private final int selectionStart;
private final int selectionEnd;
private final int selectionAnchor;
private final int selectionFocus;
private final int composingStart;
private final int composingEnd;

Expand Down Expand Up @@ -66,8 +66,8 @@ public TextInputState(String text, int selectionStart, int selectionEnd) {
/// - `composingEnd`: UTF-16 offset where the marked text ends, or -1 when none
public TextInputState(String text, int selectionStart, int selectionEnd, int composingStart, int composingEnd) {
this.text = text == null ? "" : text;
this.selectionStart = selectionStart;
this.selectionEnd = selectionEnd;
this.selectionAnchor = selectionStart;
this.selectionFocus = selectionEnd;
this.composingStart = composingStart;
this.composingEnd = composingEnd;
}
Expand All @@ -77,14 +77,24 @@ public String getText() {
return text;
}

/// Returns the UTF-16 offset of the selection anchor.
/// Returns the lower UTF-16 selection offset, independent of selection direction.
public int getSelectionStart() {
return selectionStart;
return Math.min(selectionAnchor, selectionFocus);
}

/// Returns the UTF-16 offset of the selection focus.
/// Returns the upper UTF-16 selection offset, independent of selection direction.
public int getSelectionEnd() {
return selectionEnd;
return Math.max(selectionAnchor, selectionFocus);
Comment thread
shai-almog marked this conversation as resolved.
}

/// Returns the UTF-16 anchor offset, which may be greater than the focus.
public int getSelectionAnchor() {
return selectionAnchor;
}

/// Returns the UTF-16 focus offset (the moving end of a selection).
public int getSelectionFocus() {
return selectionFocus;
}

/// Returns the UTF-16 offset where the IME marked text begins, or -1 when no composition is active.
Expand All @@ -99,7 +109,7 @@ public int getComposingEnd() {

/// True when the selection is collapsed to a caret.
public boolean isCaret() {
return selectionStart == selectionEnd;
return selectionAnchor == selectionFocus;
}

/// True when an IME composition (marked text) is currently active.
Expand Down
194 changes: 170 additions & 24 deletions CodenameOne/src/com/codename1/ui/editor/EditorView.java
Original file line number Diff line number Diff line change
Expand Up @@ -1061,6 +1061,16 @@ private int caretVertical(int delta) {
return doc.getLineStart(target) + Math.min(col, lineLen);
}

private int caretPage(boolean down) {
int line = doc.lineOfOffset(caret);
Font font = getEditorFont();
int x = lineContentOffsetX(line) + measureColumnX(line, doc.getLineText(line),
caret - doc.getLineStart(line), font);
int target = lineAtContentY(lineTopContent(line) + (down ? 1 : -1) * Math.max(1, contentHeight()));
return doc.getLineStart(target) + columnAtX(target, doc.getLineText(target),
x - lineContentOffsetX(target), font);
}

private void lineHome(boolean extend) {
moveCaret(doc.getLineStart(doc.lineOfOffset(caret)), extend);
}
Expand Down Expand Up @@ -1747,41 +1757,125 @@ private boolean handleNavKey(int keyCode) {
return false;
}

private static boolean isWordChar(char c) {
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_';
private int wordCodePointAt(int offset) {
char first = doc.charAt(offset);
if (Character.isHighSurrogate(first) && offset + 1 < doc.length()
&& Character.isLowSurrogate(doc.charAt(offset + 1))) {
return Character.toCodePoint(first, doc.charAt(offset + 1));
}
return first;
}

private int previousWordOffset(int offset) {
int previous = offset - 1;
if (previous > 0 && Character.isLowSurrogate(doc.charAt(previous))
&& Character.isHighSurrogate(doc.charAt(previous - 1))) {
previous--;
}
return previous;
}

private boolean wordCharacterAt(int offset) {
int cp = wordCodePointAt(offset);
while (WordCharacters.isMark(cp)) {
if (offset == 0) {
return false;
}
offset = previousWordOffset(offset);
cp = wordCodePointAt(offset);
}
return WordCharacters.contains(cp);
}

private int visualWord(boolean right) {
int line = doc.lineOfOffset(caret);
int start = doc.getLineStart(line);
String text = doc.getLineText(line);
Font font = getEditorFont();
int x = measureColumnX(line, text, caret - start, font);
int best = caret;
int distance = Integer.MAX_VALUE;
// Examine word edges in rendered order, so mixed-direction runs retain
// spatial left/right semantics instead of reversing the whole paragraph.
int[][] runs = bidiRuns(text, font);
for (int[] run : runs) {
int p = run[0];
while (p < run[1]) {
if (!wordCharacterAt(start + p)) {
p += Character.charCount(wordCodePointAt(start + p));
continue;
}
int lo = p;
do {
p += Character.charCount(wordCodePointAt(start + p));
} while (p < run[1] && wordCharacterAt(start + p));
boolean rtl = (run[2] & 1) != 0;
int edge = right == rtl ? lo : p;
int[] segments = lineVisualSegments(line, text, lo, p, font);
int edgeX = right ? Integer.MIN_VALUE : Integer.MAX_VALUE;
for (int i = 0; i < segments.length; i += 2) {
edgeX = right ? Math.max(edgeX, segments[i] + segments[i + 1])
: Math.min(edgeX, segments[i]);
}
int delta = right ? edgeX - x : x - edgeX;
if (delta > 0 && delta < distance) {
distance = delta;
best = start + edge;
}
}
}
if (best != caret) {
return best;
}
// A line can end in punctuation or contain no word characters at all.
if (runs.length > 0) {
int[] edgeRun = runs[right ? runs.length - 1 : 0];
boolean rtl = (edgeRun[2] & 1) != 0;
int edge = start + edgeRun[right == rtl ? 0 : 1];
if (edge != caret) {
return edge;
}
}
// At the visual line edge continue in the paragraph's reading order.
boolean forward = right != isRTL();
int adjacent = line + (forward ? 1 : -1);
if (adjacent >= 0 && adjacent < doc.getLineCount()) {
return forward ? doc.getLineStart(adjacent) : doc.getLineEnd(adjacent);
}
return caret;
}

private int wordLeft() {
int p = caret;
while (p > 0 && !isWordChar(doc.charAt(p - 1))) {
p--;
while (p > 0 && !wordCharacterAt(previousWordOffset(p))) {
p = previousWordOffset(p);
}
while (p > 0 && isWordChar(doc.charAt(p - 1))) {
p--;
while (p > 0 && wordCharacterAt(previousWordOffset(p))) {
p = previousWordOffset(p);
}
return p;
}

private int wordRight() {
int p = caret;
int n = doc.length();
while (p < n && !isWordChar(doc.charAt(p))) {
p++;
while (p < n && !wordCharacterAt(p)) {
p += Character.charCount(wordCodePointAt(p));
}
while (p < n && isWordChar(doc.charAt(p))) {
p++;
while (p < n && wordCharacterAt(p)) {
p += Character.charCount(wordCodePointAt(p));
}
return p;
}

private void selectWordAt(int offset) {
int s = offset;
int e = offset;
while (s > 0 && isWordChar(doc.charAt(s - 1))) {
s--;
while (s > 0 && wordCharacterAt(previousWordOffset(s))) {
s = previousWordOffset(s);
}
while (e < doc.length() && isWordChar(doc.charAt(e))) {
e++;
while (e < doc.length() && wordCharacterAt(e)) {
e += Character.charCount(wordCodePointAt(e));
}
anchor = s;
caret = e;
Expand Down Expand Up @@ -1976,6 +2070,20 @@ public void deleteSurroundingText(int before, int after) {
replaceRange(s, e, "", true);
}

private int paragraphBoundary(boolean forward, boolean advance) {
int line = doc.lineOfOffset(caret);
if (forward) {
if (advance && caret == doc.getLineEnd(line) && line + 1 < doc.getLineCount()) {
line++;
}
return doc.getLineEnd(line);
}
if (advance && caret == doc.getLineStart(line) && line > 0) {
line--;
}
return doc.getLineStart(line);
}

@Override
public void onKeyCommand(int command, int modifiers) {
boolean extend = (modifiers & MOD_SHIFT) != 0;
Expand All @@ -1985,6 +2093,12 @@ public void onKeyCommand(int command, int modifiers) {
boolean lineJump = (modifiers & MOD_CTRL) != 0;
boolean word = (modifiers & MOD_ALT) != 0;
switch (command) {
case KEY_WORD_LEFT:
moveCaret(visualWord(false), extend);
break;
case KEY_WORD_RIGHT:
moveCaret(visualWord(true), extend);
break;
case KEY_LEFT:
if (lineJump) {
lineHome(extend);
Expand All @@ -2004,28 +2118,60 @@ public void onKeyCommand(int command, int modifiers) {
}
break;
case KEY_UP:
moveCaret(lineJump ? 0 : caretVertical(-1), extend);
moveCaret(lineJump ? 0 : word ? paragraphBoundary(false, true) : caretVertical(-1), extend);
break;
case KEY_DOWN:
moveCaret(lineJump ? doc.length() : caretVertical(1), extend);
moveCaret(lineJump ? doc.length() : word ? paragraphBoundary(true, true) : caretVertical(1), extend);
break;
case KEY_HOME:
lineHome(extend);
if (lineJump) {
moveCaret(0, extend);
} else if (word) {
moveCaret(paragraphBoundary(false, false), extend);
} else {
lineHome(extend);
}
break;
case KEY_END:
lineEndKey(extend);
if (lineJump) {
moveCaret(doc.length(), extend);
} else if (word) {
moveCaret(paragraphBoundary(true, false), extend);
} else {
lineEndKey(extend);
}
break;
case KEY_PAGE_UP:
moveCaret(caretVertical(-Math.max(1, contentHeight() / getLineHeight())), extend);
moveCaret(caretPage(false), extend);
break;
case KEY_PAGE_DOWN:
moveCaret(caretVertical(Math.max(1, contentHeight() / getLineHeight())), extend);
moveCaret(caretPage(true), extend);
break;
case KEY_SCROLL_PAGE_UP:
scrollByPixels(0, Math.max(1, contentHeight()));
break;
case KEY_SCROLL_PAGE_DOWN:
scrollByPixels(0, -Math.max(1, contentHeight()));
break;
case KEY_BACKSPACE:
deleteBackward();
if (word && !hasSelection() && editable) {
int start = wordLeft();
if (start != caret) {
replaceRange(start, caret, "", true);
}
} else {
deleteBackward();
}
break;
case KEY_DELETE:
deleteForward();
if (word && !hasSelection() && editable) {
int end = wordRight();
if (end != caret) {
replaceRange(caret, end, "", true);
}
} else {
deleteForward();
}
break;
case KEY_COPY:
copySelection();
Expand Down Expand Up @@ -2090,7 +2236,7 @@ public void onEditorAction(int action) {

@Override
public TextInputState getEditingState() {
return new TextInputState(doc.getText(), getSelectionStart(), getSelectionEnd(), composingStart, composingEnd);
return new TextInputState(doc.getText(), anchor < 0 ? caret : anchor, caret, composingStart, composingEnd);
Comment thread
shai-almog marked this conversation as resolved.
}

/// Measures all insertion positions with one bulk request per directional run. Subclasses
Expand Down Expand Up @@ -2158,7 +2304,7 @@ public com.codename1.ui.TextInputGeometry getGeometrySnapshot() {
System.arraycopy(line, 0, points, at, line.length);
at += line.length;
}
return new com.codename1.ui.TextInputGeometry(points, getSelectionStart(), getSelectionEnd(),
return new com.codename1.ui.TextInputGeometry(points, anchor < 0 ? caret : anchor, caret,
selectionRects(getSelectionStart(), getSelectionEnd(), first, last), getCaretRect());
}

Expand Down
Loading
Loading