Make selection end handle stick to selection at line end.
At line break, one offset can be mapped to two phisical
position: previous line end and next line start.
Previously, all cursor handles are placed at next line
start.
With this CL, selection end handle is placed at the
previous line end in such cases.
Test: FrameworksCoreTests
Bug: 21305922
Change-Id: I00d9f9a0cd417ca92534e93b3d3f655cd62f25d3
diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java
index fd6fc7d..fb35fed 100644
--- a/core/java/android/text/Layout.java
+++ b/core/java/android/text/Layout.java
@@ -908,17 +908,24 @@
* the paragraph's primary direction.
*/
public float getPrimaryHorizontal(int offset) {
- return getPrimaryHorizontal(offset, false /* not clamped */);
+ return getPrimaryHorizontal(offset, false /* not clamped */,
+ true /* getNewLineStartPosOnLineBreak */);
}
/**
* Get the primary horizontal position for the specified text offset, but
* optionally clamp it so that it doesn't exceed the width of the layout.
+ *
+ * @param offset the offset to get horizontal position
+ * @param clamped whether to clamp the position by using the width of this layout.
+ * @param getNewLineStartPosOnLineBreak whether to get the start position of new line when the
+ * offset is at automatic line break.
* @hide
*/
- public float getPrimaryHorizontal(int offset, boolean clamped) {
+ public float getPrimaryHorizontal(int offset, boolean clamped,
+ boolean getNewLineStartPosOnLineBreak) {
boolean trailing = primaryIsTrailingPrevious(offset);
- return getHorizontal(offset, trailing, clamped);
+ return getHorizontal(offset, trailing, clamped, getNewLineStartPosOnLineBreak);
}
/**
@@ -927,26 +934,37 @@
* the direction other than the paragraph's primary direction.
*/
public float getSecondaryHorizontal(int offset) {
- return getSecondaryHorizontal(offset, false /* not clamped */);
+ return getSecondaryHorizontal(offset, false /* not clamped */,
+ true /* getNewLineStartPosOnLineBreak */);
}
/**
* Get the secondary horizontal position for the specified text offset, but
* optionally clamp it so that it doesn't exceed the width of the layout.
+ *
+ * @param offset the offset to get horizontal position
+ * @param clamped whether to clamp the position by using the width of this layout.
+ * @param getNewLineStartPosOnLineBreak whether to get the start position of new line when the
+ * offset is at automatic line break.
* @hide
*/
- public float getSecondaryHorizontal(int offset, boolean clamped) {
+ public float getSecondaryHorizontal(int offset, boolean clamped,
+ boolean getNewLineStartPosOnLineBreak) {
boolean trailing = primaryIsTrailingPrevious(offset);
- return getHorizontal(offset, !trailing, clamped);
+ return getHorizontal(offset, !trailing, clamped, getNewLineStartPosOnLineBreak);
}
- private float getHorizontal(int offset, boolean primary) {
- return primary ? getPrimaryHorizontal(offset) : getSecondaryHorizontal(offset);
+ private float getHorizontal(int offset, boolean primary,
+ boolean getNewLineStartPosOnLineBreak) {
+ return primary ? getPrimaryHorizontal(offset, false /* not clamped */,
+ getNewLineStartPosOnLineBreak)
+ : getSecondaryHorizontal(offset, false /* not clamped */,
+ getNewLineStartPosOnLineBreak);
}
- private float getHorizontal(int offset, boolean trailing, boolean clamped) {
- int line = getLineForOffset(offset);
-
+ private float getHorizontal(int offset, boolean trailing, boolean clamped,
+ boolean getNewLineStartPosOnLineBreak) {
+ final int line = getLineForOffset(offset, getNewLineStartPosOnLineBreak);
return getHorizontal(offset, trailing, line, clamped);
}
@@ -1150,6 +1168,10 @@
* beyond the end of the text, you get the last line.
*/
public int getLineForOffset(int offset) {
+ return getLineForOffset(offset, true);
+ }
+
+ private int getLineForOffset(int offset, boolean getNewLineOnLineBreak) {
int high = getLineCount(), low = -1, guess;
while (high - low > 1) {
@@ -1161,10 +1183,15 @@
low = guess;
}
- if (low < 0)
+ if (low < 0) {
return 0;
- else
+ } else {
+ if (!getNewLineOnLineBreak && low > 0 && getLineStart(low) == offset
+ && mText.charAt(offset - 1) != '\n') {
+ return low - 1;
+ }
return low;
+ }
}
/**
@@ -1198,14 +1225,14 @@
false, null);
final int max;
- if (line == getLineCount() - 1) {
- max = lineEndOffset;
- } else {
+ if (line != getLineCount() - 1 && mText.charAt(lineEndOffset - 1) == '\n') {
max = tl.getOffsetToLeftRightOf(lineEndOffset - lineStartOffset,
!isRtlCharAt(lineEndOffset - 1)) + lineStartOffset;
+ } else {
+ max = lineEndOffset;
}
int best = lineStartOffset;
- float bestdist = Math.abs(getHorizontal(best, primary) - horiz);
+ float bestdist = Math.abs(getHorizontal(best, primary, true) - horiz);
for (int i = 0; i < dirs.mDirections.length; i += 2) {
int here = lineStartOffset + dirs.mDirections[i];
@@ -1221,10 +1248,13 @@
guess = (high + low) / 2;
int adguess = getOffsetAtStartOf(guess);
- if (getHorizontal(adguess, primary) * swap >= horiz * swap)
+ if (getHorizontal(adguess, primary,
+ adguess == lineStartOffset || adguess != lineEndOffset) * swap
+ >= horiz * swap) {
high = guess;
- else
+ } else {
low = guess;
+ }
}
if (low < here + 1)
@@ -1234,9 +1264,11 @@
int aft = tl.getOffsetToLeftRightOf(low - lineStartOffset, isRtl) + lineStartOffset;
low = tl.getOffsetToLeftRightOf(aft - lineStartOffset, !isRtl) + lineStartOffset;
if (low >= here && low < there) {
- float dist = Math.abs(getHorizontal(low, primary) - horiz);
+ float dist = Math.abs(getHorizontal(low, primary,
+ low == lineStartOffset || low != lineEndOffset) - horiz);
if (aft < there) {
- float other = Math.abs(getHorizontal(aft, primary) - horiz);
+ float other = Math.abs(getHorizontal(aft, primary,
+ aft == lineStartOffset || aft != lineEndOffset) - horiz);
if (other < dist) {
dist = other;
@@ -1251,7 +1283,8 @@
}
}
- float dist = Math.abs(getHorizontal(here, primary) - horiz);
+ float dist = Math.abs(getHorizontal(here, primary,
+ here == lineStartOffset || here != lineEndOffset) - horiz);
if (dist < bestdist) {
bestdist = dist;
@@ -1259,10 +1292,10 @@
}
}
- float dist = Math.abs(getHorizontal(max, primary) - horiz);
+ float dist = Math.abs(getHorizontal(max, primary,
+ max == lineStartOffset || max != lineEndOffset) - horiz);
if (dist <= bestdist) {
- bestdist = dist;
best = max;
}
@@ -1459,8 +1492,9 @@
int bottom = getLineTop(line+1);
boolean clamped = shouldClampCursor(line);
- float h1 = getPrimaryHorizontal(point, clamped) - 0.5f;
- float h2 = isLevelBoundary(point) ? getSecondaryHorizontal(point, clamped) - 0.5f : h1;
+ float h1 = getPrimaryHorizontal(point, clamped, true) - 0.5f;
+ float h2 = isLevelBoundary(point)
+ ? getSecondaryHorizontal(point, clamped, true) - 0.5f : h1;
int caps = TextKeyListener.getMetaState(editingBuffer, TextKeyListener.META_SHIFT_ON) |
TextKeyListener.getMetaState(editingBuffer, TextKeyListener.META_SELECTING);
@@ -1577,8 +1611,7 @@
}
int startline = getLineForOffset(start);
- int endline = getLineForOffset(end);
-
+ int endline = getLineForOffset(end, false);
int top = getLineTop(startline);
int bottom = getLineBottom(endline);
diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java
index 5eaabe7..c04347c 100644
--- a/core/java/android/widget/Editor.java
+++ b/core/java/android/widget/Editor.java
@@ -1931,10 +1931,11 @@
}
boolean clamped = layout.shouldClampCursor(line);
- updateCursorPosition(0, top, middle, layout.getPrimaryHorizontal(offset, clamped));
+ updateCursorPosition(0, top, middle, layout.getPrimaryHorizontal(offset, clamped, true));
if (mCursorCount == 2) {
- updateCursorPosition(1, middle, bottom, layout.getSecondaryHorizontal(offset, clamped));
+ updateCursorPosition(1, middle, bottom,
+ layout.getSecondaryHorizontal(offset, clamped, true));
}
}
@@ -4331,7 +4332,7 @@
updateSelection(offset);
addPositionToTouchUpFilter(offset);
}
- final int line = layout.getLineForOffset(offset);
+ final int line = getLineForOffset(layout, offset);
mPrevLine = line;
mPositionX = getCursorHorizontalPosition(layout, offset) - mHotspotX
@@ -4358,6 +4359,15 @@
return (int) (getHorizontal(layout, offset) - 0.5f);
}
+ /**
+ * @param layout Text layout.
+ * @param offset Character offset for the cursor.
+ * @return The line the cursor should be at.
+ */
+ int getLineForOffset(Layout layout, int offset) {
+ return layout.getLineForOffset(offset);
+ }
+
@Override
public void updatePosition(int parentPositionX, int parentPositionY,
boolean parentPositionChanged, boolean parentScrolled) {
@@ -4786,7 +4796,7 @@
|| !isStartHandle() && initialOffset <= anotherHandleOffset) {
// Handles have crossed, bound it to the first selected line and
// adjust by word / char as normal.
- currLine = layout.getLineForOffset(anotherHandleOffset);
+ currLine = getLineForOffset(layout, anotherHandleOffset, !isStartHandle());
initialOffset = getOffsetAtCoordinate(layout, currLine, x);
}
@@ -4858,14 +4868,18 @@
if (isExpanding) {
// User is increasing the selection.
int wordBoundary = isStartHandle() ? wordStart : wordEnd;
- final boolean snapToWord = (!mInWord
- || (isStartHandle() ? currLine < mPrevLine : currLine > mPrevLine))
- && atRtl == isAtRtlRun(layout, wordBoundary);
+ final boolean atLineBoundary = layout.getLineStart(currLine) == offset
+ || layout.getLineEnd(currLine) == offset;
+ final boolean atWordBoundary = getWordIteratorWithText().isBoundary(offset);
+ final boolean snapToWord = !(atLineBoundary && atWordBoundary)
+ && (!mInWord
+ || (isStartHandle() ? currLine < mPrevLine : currLine > mPrevLine))
+ && atRtl == isAtRtlRun(layout, wordBoundary);
if (snapToWord) {
// Sometimes words can be broken across lines (Chinese, hyphenation).
// We still snap to the word boundary but we only use the letters on the
// current line to determine if the user is far enough into the word to snap.
- if (layout.getLineForOffset(wordBoundary) != currLine) {
+ if (getLineForOffset(layout, wordBoundary) != currLine) {
wordBoundary = isStartHandle()
? layout.getLineStart(currLine) : layout.getLineEnd(currLine);
}
@@ -5013,12 +5027,29 @@
}
private float getHorizontal(@NonNull Layout layout, int offset, boolean startHandle) {
- final int line = layout.getLineForOffset(offset);
+ final int line = getLineForOffset(layout, offset);
final int offsetToCheck = startHandle ? offset : Math.max(offset - 1, 0);
final boolean isRtlChar = layout.isRtlCharAt(offsetToCheck);
final boolean isRtlParagraph = layout.getParagraphDirection(line) == -1;
return (isRtlChar == isRtlParagraph)
- ? layout.getPrimaryHorizontal(offset) : layout.getSecondaryHorizontal(offset);
+ ? layout.getPrimaryHorizontal(offset, false, startHandle)
+ : layout.getSecondaryHorizontal(offset, false, startHandle);
+ }
+
+ @Override
+ public int getLineForOffset(@NonNull Layout layout, int offset) {
+ return getLineForOffset(layout, offset, isStartHandle());
+ }
+
+ private int getLineForOffset(@NonNull Layout layout, int offset, boolean startHandle) {
+ final int line = layout.getLineForOffset(offset);
+ if (!startHandle && line > 0 && layout.getLineStart(line) == offset
+ && mTextView.getText().charAt(offset - 1) != '\n') {
+ // If end handle is at a line break in a paragraph, the handle should be at the
+ // previous line.
+ return line - 1;
+ }
+ return line;
}
@Override
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index 5426a37..e908911 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -7719,7 +7719,7 @@
// right where it is most likely to be annoying.
final boolean clamped = grav > 0;
// FIXME: Is it okay to truncate this, or should we round?
- final int x = (int) layout.getPrimaryHorizontal(offset, clamped);
+ final int x = (int) layout.getPrimaryHorizontal(offset, clamped, true);
final int top = layout.getLineTop(line);
final int bottom = layout.getLineTop(line + 1);
diff --git a/core/tests/coretests/src/android/widget/TextViewActivityTest.java b/core/tests/coretests/src/android/widget/TextViewActivityTest.java
index 71dd526..b276d16 100644
--- a/core/tests/coretests/src/android/widget/TextViewActivityTest.java
+++ b/core/tests/coretests/src/android/widget/TextViewActivityTest.java
@@ -26,6 +26,7 @@
import static android.widget.espresso.TextViewActions.Handle;
import static android.widget.espresso.TextViewActions.longPressAndDragOnText;
import static android.widget.espresso.TextViewActions.longPressOnTextAtIndex;
+import static android.widget.espresso.TextViewAssertions.handleIsOnLine;
import static android.widget.espresso.TextViewAssertions.hasInsertionPointerAtIndex;
import static android.widget.espresso.TextViewAssertions.hasSelection;
import static android.widget.espresso.FloatingToolbarEspressoUtils.assertFloatingToolbarIsDisplayed;
@@ -464,6 +465,28 @@
onView(withId(R.id.textview)).check(hasSelection("abcd\nefg\nhijk\nlmn\nopqr"));
}
+ public void testSelectionHandles_multiLine_japanese() throws Exception {
+ onView(withId(R.id.textview)).perform(click());
+ final TextView textView = (TextView) getActivity().findViewById(R.id.textview);
+ final StringBuilder builder = new StringBuilder();
+ for (int i = 0; i < 100; ++i) {
+ builder.append("\u3042\u3044\u3046\u3048\u304A");
+ onView(withId(R.id.textview)).perform(replaceText(builder.toString()));
+ final int lineEnd = textView.getLayout().getLineEnd(0);
+ if (lineEnd < builder.length()) {
+ break;
+ }
+ }
+
+ onView(withId(R.id.textview)).perform(doubleClickOnTextAtIndex(3));
+
+ final int lineEnd = textView.getLayout().getLineEnd(0);
+ onHandleView(com.android.internal.R.id.selection_end_handle)
+ .perform(dragHandle(textView, Handle.SELECTION_END, lineEnd, true, false));
+ onHandleView(com.android.internal.R.id.selection_end_handle)
+ .check(handleIsOnLine(textView, 0));
+ }
+
public void testSelectionHandles_multiLine_rtl() throws Exception {
// Arabic text.
final String text = "\u062A\u062B\u062C\n" + "\u062D\u062E\u062F\n"
diff --git a/core/tests/coretests/src/android/widget/espresso/TextViewActions.java b/core/tests/coretests/src/android/widget/espresso/TextViewActions.java
index 335d021..1e88712 100644
--- a/core/tests/coretests/src/android/widget/espresso/TextViewActions.java
+++ b/core/tests/coretests/src/android/widget/espresso/TextViewActions.java
@@ -331,15 +331,37 @@
*/
public static ViewAction dragHandle(TextView textView, Handle handleType, int endIndex,
boolean primary) {
+ return dragHandle(textView, handleType, endIndex, primary, true);
+ }
+
+ /**
+ * Returns an action that tap then drags on the handle from the current position to endIndex on
+ * the TextView.<br>
+ * <br>
+ * View constraints:
+ * <ul>
+ * <li>must be a TextView's drag-handle displayed on screen
+ * <ul>
+ *
+ * @param textView TextView the handle is on
+ * @param handleType Type of the handle
+ * @param endIndex The index of the TextView's text to end the drag at
+ * @param primary whether to use primary direction to get coordinate form index when endIndex is
+ * at a direction boundary.
+ * @param getNewLineStartPosOnLineBreak whether to use new line start coordinate on a line break
+ * within a paragraph.
+ */
+ public static ViewAction dragHandle(TextView textView, Handle handleType, int endIndex,
+ boolean primary, boolean getNewLineStartPosOnLineBreak) {
return actionWithAssertions(
new DragAction(
DragAction.Drag.TAP,
new CurrentHandleCoordinates(textView),
- new HandleCoordinates(textView, handleType, endIndex, primary),
+ new HandleCoordinates(textView, handleType, endIndex, primary,
+ getNewLineStartPosOnLineBreak),
Press.FINGER,
Editor.HandleView.class));
}
-
/**
* A provider of the x, y coordinates of the handle dragging point.
*/
@@ -402,13 +424,16 @@
private final Handle mHandleType;
private final int mIndex;
private final boolean mPrimary;
+ private final boolean mGetNewLineStartPosOnLineBreak;
private final String mActionDescription;
- public HandleCoordinates(TextView textView, Handle handleType, int index, boolean primary) {
+ public HandleCoordinates(TextView textView, Handle handleType, int index, boolean primary,
+ boolean getNewLineStartPosOnLineBreak) {
mTextView = textView;
mHandleType = handleType;
mIndex = index;
mPrimary = primary;
+ mGetNewLineStartPosOnLineBreak = getNewLineStartPosOnLineBreak;
mActionDescription = "Could not locate " + handleType.toString()
+ " handle that points text index: " + index
+ " (" + (primary ? "primary" : "secondary" ) + ")";
@@ -445,9 +470,10 @@
final float currentX = handleView.getHorizontal(layout, currentOffset);
final float currentY = layout.getLineTop(currentLine);
final float[] currentCoordinates =
- TextCoordinates.convertToScreenCoordinates(mTextView, currentX, currentY);
+ convertToScreenCoordinates(mTextView, currentX, currentY);
final float[] targetCoordinates =
- (new TextCoordinates(mIndex, mPrimary)).calculateCoordinates(mTextView);
+ (new TextCoordinates(mIndex, mPrimary, mGetNewLineStartPosOnLineBreak))
+ .calculateCoordinates(mTextView);
final Rect bounds = new Rect();
view.getBoundsOnScreen(bounds);
final Rect visibleDisplayBounds = new Rect();
@@ -485,23 +511,27 @@
private final int mIndex;
private final boolean mPrimary;
+ private final boolean mGetNewLineStartPosOnLineBreak;
private final String mActionDescription;
public TextCoordinates(int index) {
- this(index, true);
+ this(index, true, true);
}
- public TextCoordinates(int index, boolean primary) {
+ public TextCoordinates(int index, boolean primary, boolean getNewLineStartPosOnLineBreak) {
mIndex = index;
mPrimary = primary;
+ mGetNewLineStartPosOnLineBreak = getNewLineStartPosOnLineBreak;
mActionDescription = "Could not locate text at index: " + mIndex
- + " (" + (primary ? "primary" : "secondary" ) + ")";
+ + " (" + (primary ? "primary" : "secondary" )
+ + ", mGetNewLineStartPosOnLineBreak: " + mGetNewLineStartPosOnLineBreak + ")";
}
@Override
public float[] calculateCoordinates(View view) {
try {
- return locateTextAtIndex((TextView) view, mIndex, mPrimary);
+ return locateTextAtIndex((TextView) view, mIndex, mPrimary,
+ mGetNewLineStartPosOnLineBreak);
} catch (ClassCastException e) {
throw new PerformException.Builder()
.withActionDescription(mActionDescription)
@@ -520,30 +550,38 @@
/**
* @throws StringIndexOutOfBoundsException
*/
- private float[] locateTextAtIndex(TextView textView, int index, boolean primary) {
+ private float[] locateTextAtIndex(TextView textView, int index, boolean primary,
+ boolean getNewLineStartPosOnLineBreak) {
if (index < 0 || index > textView.getText().length()) {
throw new StringIndexOutOfBoundsException(index);
}
final Layout layout = textView.getLayout();
- final int line = layout.getLineForOffset(index);
+
+ int line = layout.getLineForOffset(index);
+ if (!getNewLineStartPosOnLineBreak && line > 0 && layout.getLineStart(line) == index
+ && textView.getText().charAt(index - 1) != '\n') {
+ line = line - 1;
+ }
return convertToScreenCoordinates(textView,
- (primary ? layout.getPrimaryHorizontal(index)
- : layout.getSecondaryHorizontal(index)),
+ (primary ? layout.getPrimaryHorizontal(index, false,
+ getNewLineStartPosOnLineBreak)
+ : layout.getSecondaryHorizontal(index, false,
+ getNewLineStartPosOnLineBreak)),
layout.getLineTop(line));
}
+ }
- /**
- * Convert TextView's local coordinates to on screen coordinates.
- * @param textView the TextView
- * @param x local horizontal coordinate
- * @param y local vertical coordinate
- * @return
- */
- public static float[] convertToScreenCoordinates(TextView textView, float x, float y) {
- final int[] xy = new int[2];
- textView.getLocationOnScreen(xy);
- return new float[]{ x + textView.getTotalPaddingLeft() - textView.getScrollX() + xy[0],
- y + textView.getTotalPaddingTop() - textView.getScrollY() + xy[1] };
- }
+ /**
+ * Convert TextView's local coordinates to on screen coordinates.
+ * @param textView the TextView
+ * @param x local horizontal coordinate
+ * @param y local vertical coordinate
+ * @return
+ */
+ public static float[] convertToScreenCoordinates(TextView textView, float x, float y) {
+ final int[] xy = new int[2];
+ textView.getLocationOnScreen(xy);
+ return new float[]{ x + textView.getTotalPaddingLeft() - textView.getScrollX() + xy[0],
+ y + textView.getTotalPaddingTop() - textView.getScrollY() + xy[1] };
}
}
diff --git a/core/tests/coretests/src/android/widget/espresso/TextViewAssertions.java b/core/tests/coretests/src/android/widget/espresso/TextViewAssertions.java
index 6e44cd8..fef84f4 100644
--- a/core/tests/coretests/src/android/widget/espresso/TextViewAssertions.java
+++ b/core/tests/coretests/src/android/widget/espresso/TextViewAssertions.java
@@ -28,11 +28,14 @@
import android.support.test.espresso.ViewAssertion;
import android.view.View;
import android.widget.EditText;
+import android.widget.Editor;
import android.widget.TextView;
import junit.framework.AssertionFailedError;
import org.hamcrest.Matcher;
+import com.android.ex.editstyledtext.EditStyledText.EditModeActions.TextViewAction;
+
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -137,6 +140,14 @@
}
/**
+ * Returns a {@link ViewAssertion} that asserts that the TextView selection handle is on the
+ * specified line.
+ */
+ public static ViewAssertion handleIsOnLine(TextView tv, int line) {
+ return new SelectionHandlePositionAssertion(tv, line);
+ }
+
+ /**
* A {@link ViewAssertion} to check the selected text in a {@link TextView}.
*/
private static final class TextSelectionAssertion implements ViewAssertion {
@@ -216,4 +227,31 @@
closeTo(0f, 1f));
}
}
+ /**
+ * {@link ViewAssertion} to check that TextView selection handle is on a given line.
+ */
+ static class SelectionHandlePositionAssertion implements ViewAssertion {
+ private TextView mTextView;
+ private int mLine;
+ private SelectionHandlePositionAssertion(TextView tv, int line) {
+ mTextView = tv;
+ mLine = line;
+ }
+
+ @Override
+ public void check(View view, NoMatchingViewException exception) {
+ if (!(view instanceof Editor.HandleView)) {
+ throw new AssertionFailedError("View should be an instance of Editor.HandleView");
+ }
+ final Editor.HandleView handleView = (Editor.HandleView) view;
+ final Rect bounds = new Rect();
+ handleView.getBoundsOnScreen(bounds);
+ final float bottom = mTextView.getLayout().getLineBottom(mLine);
+ final float[] pos =
+ TextViewActions.convertToScreenCoordinates(mTextView, 0, bottom);
+
+ assertThat("Cursor should be on the line " + mLine, Double.valueOf(bounds.top),
+ closeTo(pos[1], 1f));
+ }
+ }
}