Merge "TextView: Offer suggestions in context menu."

This commit is contained in:
Keisuke Kuroyanagi
2016-01-13 08:02:00 +00:00
committed by Android (Google) Code Review

View File

@@ -82,6 +82,7 @@ import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.RenderNode;
import android.view.SubMenu;
import android.view.View;
import android.view.View.DragShadowBuilder;
import android.view.View.OnClickListener;
@@ -285,6 +286,9 @@ public class Editor {
boolean mIsInsertionActionModeStartPending = false;
private final SuggestionHelper mSuggestionHelper = new SuggestionHelper();
private SuggestionInfo[] mSuggestionInfosInContextMenu;
Editor(TextView textView) {
mTextView = textView;
// Synchronize the filter list, which places the undo input filter at the end.
@@ -2369,6 +2373,9 @@ public class Editor {
if (offset == -1) {
return;
}
mPreserveDetachedSelection = true;
stopTextActionMode();
mPreserveDetachedSelection = false;
final boolean isOnSelection = mTextView.hasSelection()
&& offset >= mTextView.getSelectionStart() && offset <= mTextView.getSelectionEnd();
if (!isOnSelection) {
@@ -2378,7 +2385,24 @@ public class Editor {
Selection.setSelection((Spannable) mTextView.getText(), offset);
}
// TODO: Add suggestions in the context menu.
if (shouldOfferToShowSuggestions()) {
if (mSuggestionInfosInContextMenu == null) {
mSuggestionInfosInContextMenu =
new SuggestionInfo[SuggestionSpan.SUGGESTIONS_MAX_SIZE];
for (int i = 0; i < mSuggestionInfosInContextMenu.length; i++) {
mSuggestionInfosInContextMenu[i] = new SuggestionInfo();
}
}
final SubMenu subMenu = menu.addSubMenu(Menu.NONE, Menu.NONE, MENU_ITEM_ORDER_REPLACE,
com.android.internal.R.string.replace);
mSuggestionHelper.getSuggestionInfo(mSuggestionInfosInContextMenu);
int i = 0;
for (final SuggestionInfo info : mSuggestionInfosInContextMenu) {
info.mSuggestionEnd = info.mText.length();
subMenu.add(Menu.NONE, Menu.NONE, i++, info.mText)
.setOnMenuItemClickListener(mOnContextMenuReplaceItemClickListener);
}
}
menu.add(Menu.NONE, TextView.ID_UNDO, MENU_ITEM_ORDER_UNDO,
com.android.internal.R.string.undo)
@@ -2422,6 +2446,61 @@ public class Editor {
mPreserveDetachedSelection = true;
}
private void replaceWithSuggestion(SuggestionInfo suggestionInfo, int spanStart, int spanEnd) {
final Editable editable = (Editable) mTextView.getText();
final String originalText = TextUtils.substring(editable, spanStart, spanEnd);
// SuggestionSpans are removed by replace: save them before
SuggestionSpan[] suggestionSpans = editable.getSpans(spanStart, spanEnd,
SuggestionSpan.class);
final int length = suggestionSpans.length;
int[] suggestionSpansStarts = new int[length];
int[] suggestionSpansEnds = new int[length];
int[] suggestionSpansFlags = new int[length];
for (int i = 0; i < length; i++) {
final SuggestionSpan suggestionSpan = suggestionSpans[i];
suggestionSpansStarts[i] = editable.getSpanStart(suggestionSpan);
suggestionSpansEnds[i] = editable.getSpanEnd(suggestionSpan);
suggestionSpansFlags[i] = editable.getSpanFlags(suggestionSpan);
// Remove potential misspelled flags
int suggestionSpanFlags = suggestionSpan.getFlags();
if ((suggestionSpanFlags & SuggestionSpan.FLAG_MISSPELLED) != 0) {
suggestionSpanFlags &= ~SuggestionSpan.FLAG_MISSPELLED;
suggestionSpanFlags &= ~SuggestionSpan.FLAG_EASY_CORRECT;
suggestionSpan.setFlags(suggestionSpanFlags);
}
}
// Notify source IME of the suggestion pick. Do this before swapping texts.
suggestionInfo.mSuggestionSpan.notifySelection(
mTextView.getContext(), originalText, suggestionInfo.mSuggestionIndex);
// Swap text content between actual text and Suggestion span
final int suggestionStart = suggestionInfo.mSuggestionStart;
final int suggestionEnd = suggestionInfo.mSuggestionEnd;
final String suggestion = suggestionInfo.mText.subSequence(
suggestionStart, suggestionEnd).toString();
mTextView.replaceText_internal(spanStart, spanEnd, suggestion);
String[] suggestions = suggestionInfo.mSuggestionSpan.getSuggestions();
suggestions[suggestionInfo.mSuggestionIndex] = originalText;
// Restore previous SuggestionSpans
final int lengthDelta = suggestion.length() - (spanEnd - spanStart);
for (int i = 0; i < length; i++) {
// Only spans that include the modified region make sense after replacement
// Spans partially included in the replaced region are removed, there is no
// way to assign them a valid range after replacement
if (suggestionSpansStarts[i] <= spanStart && suggestionSpansEnds[i] >= spanEnd) {
mTextView.setSpan_internal(suggestionSpans[i], suggestionSpansStarts[i],
suggestionSpansEnds[i] + lengthDelta, suggestionSpansFlags[i]);
}
}
// Move cursor at the end of the replaced word
final int newCursorPosition = spanEnd + lengthDelta;
mTextView.setCursorPosition_internal(newCursorPosition, newCursorPosition);
}
private final MenuItem.OnMenuItemClickListener mOnContextMenuItemClickListener =
new MenuItem.OnMenuItemClickListener() {
@Override
@@ -2433,6 +2512,31 @@ public class Editor {
}
};
private final MenuItem.OnMenuItemClickListener mOnContextMenuReplaceItemClickListener =
new MenuItem.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem item) {
int index = item.getOrder();
if (index < 0 || index >= mSuggestionInfosInContextMenu.length) {
clear();
return false;
}
final Spannable spannable = (Spannable) mTextView.getText();
final SuggestionSpan suggestionSpan =
mSuggestionInfosInContextMenu[index].mSuggestionSpan;
replaceWithSuggestion(mSuggestionInfosInContextMenu[index],
spannable.getSpanStart(suggestionSpan), spannable.getSpanEnd(suggestionSpan));
clear();
return true;
}
private void clear() {
for (final SuggestionInfo info : mSuggestionInfosInContextMenu) {
info.clear();
}
}
};
/**
* Controls the {@link EasyEditSpan} monitoring when it is added, and when the related
* pop-up should be displayed.
@@ -2849,6 +2953,131 @@ public class Editor {
}
}
private static class SuggestionInfo {
// Range of actual suggestion within text
int mSuggestionStart, mSuggestionEnd;
// The SuggestionSpan that this TextView represents
@Nullable
SuggestionSpan mSuggestionSpan;
// The index of this suggestion inside suggestionSpan
int mSuggestionIndex;
final SpannableStringBuilder mText = new SpannableStringBuilder();
void clear() {
mSuggestionSpan = null;
mText.clear();
}
}
private class SuggestionHelper {
private final Comparator<SuggestionSpan> mSuggestionSpanComparator =
new SuggestionSpanComparator();
private final HashMap<SuggestionSpan, Integer> mSpansLengths =
new HashMap<SuggestionSpan, Integer>();
private class SuggestionSpanComparator implements Comparator<SuggestionSpan> {
public int compare(SuggestionSpan span1, SuggestionSpan span2) {
final int flag1 = span1.getFlags();
final int flag2 = span2.getFlags();
if (flag1 != flag2) {
// The order here should match what is used in updateDrawState
final boolean easy1 = (flag1 & SuggestionSpan.FLAG_EASY_CORRECT) != 0;
final boolean easy2 = (flag2 & SuggestionSpan.FLAG_EASY_CORRECT) != 0;
final boolean misspelled1 = (flag1 & SuggestionSpan.FLAG_MISSPELLED) != 0;
final boolean misspelled2 = (flag2 & SuggestionSpan.FLAG_MISSPELLED) != 0;
if (easy1 && !misspelled1) return -1;
if (easy2 && !misspelled2) return 1;
if (misspelled1) return -1;
if (misspelled2) return 1;
}
return mSpansLengths.get(span1).intValue() - mSpansLengths.get(span2).intValue();
}
}
/**
* Returns the suggestion spans that cover the current cursor position. The suggestion
* spans are sorted according to the length of text that they are attached to.
*/
private SuggestionSpan[] getSortedSuggestionSpans() {
int pos = mTextView.getSelectionStart();
Spannable spannable = (Spannable) mTextView.getText();
SuggestionSpan[] suggestionSpans = spannable.getSpans(pos, pos, SuggestionSpan.class);
mSpansLengths.clear();
for (SuggestionSpan suggestionSpan : suggestionSpans) {
int start = spannable.getSpanStart(suggestionSpan);
int end = spannable.getSpanEnd(suggestionSpan);
mSpansLengths.put(suggestionSpan, Integer.valueOf(end - start));
}
// The suggestions are sorted according to their types (easy correction first, then
// misspelled) and to the length of the text that they cover (shorter first).
Arrays.sort(suggestionSpans, mSuggestionSpanComparator);
mSpansLengths.clear();
return suggestionSpans;
}
/**
* Gets the SuggestionInfo list that contains suggestion information at the current cursor
* position.
*
* @param suggestionInfos SuggestionInfo array the results will be set.
* @return the number of suggestions actually fetched.
*/
public int getSuggestionInfo(SuggestionInfo[] suggestionInfos) {
final Spannable spannable = (Spannable) mTextView.getText();
final SuggestionSpan[] suggestionSpans = getSortedSuggestionSpans();
final int nbSpans = suggestionSpans.length;
if (nbSpans == 0) return 0;
int numberOfSuggestions = 0;
for (int spanIndex = 0; spanIndex < nbSpans; spanIndex++) {
final SuggestionSpan suggestionSpan = suggestionSpans[spanIndex];
final int spanStart = spannable.getSpanStart(suggestionSpan);
final int spanEnd = spannable.getSpanEnd(suggestionSpan);
final String[] suggestions = suggestionSpan.getSuggestions();
final int nbSuggestions = suggestions.length;
for (int suggestionIndex = 0; suggestionIndex < nbSuggestions; suggestionIndex++) {
final String suggestion = suggestions[suggestionIndex];
boolean suggestionIsDuplicate = false;
for (int i = 0; i < numberOfSuggestions; i++) {
if (suggestionInfos[i].mText.toString().equals(suggestion)) {
final SuggestionSpan otherSuggestionSpan =
suggestionInfos[i].mSuggestionSpan;
final int otherSpanStart = spannable.getSpanStart(otherSuggestionSpan);
final int otherSpanEnd = spannable.getSpanEnd(otherSuggestionSpan);
if (spanStart == otherSpanStart && spanEnd == otherSpanEnd) {
suggestionIsDuplicate = true;
break;
}
}
}
if (suggestionIsDuplicate) {
continue;
}
SuggestionInfo suggestionInfo = suggestionInfos[numberOfSuggestions];
suggestionInfo.mSuggestionSpan = suggestionSpan;
suggestionInfo.mSuggestionIndex = suggestionIndex;
suggestionInfo.mSuggestionStart = 0;
suggestionInfo.mSuggestionEnd = suggestion.length();
suggestionInfo.mText.replace(0, suggestionInfo.mText.length(), suggestion);
numberOfSuggestions++;
if (numberOfSuggestions >= suggestionInfos.length) {
return numberOfSuggestions;
}
}
}
return numberOfSuggestions;
}
}
@VisibleForTesting
public class SuggestionsPopupWindow extends PinnedPopupWindow implements OnItemClickListener {
private static final int MAX_NUMBER_SUGGESTIONS = SuggestionSpan.SUGGESTIONS_MAX_SIZE;
@@ -2862,8 +3091,6 @@ public class Editor {
private boolean mCursorWasVisibleBeforeSuggestions;
private boolean mIsShowingUp = false;
private SuggestionAdapter mSuggestionsAdapter;
private final Comparator<SuggestionSpan> mSuggestionSpanComparator;
private final HashMap<SuggestionSpan, Integer> mSpansLengths;
private final TextAppearanceSpan mHighlightSpan = new TextAppearanceSpan(
mTextView.getContext(), mTextView.mTextEditSuggestionHighlightStyle);
private TextView mAddToDictionaryButton;
@@ -2895,8 +3122,6 @@ public class Editor {
public SuggestionsPopupWindow() {
mCursorWasVisibleBeforeSuggestions = mCursorVisible;
mSuggestionSpanComparator = new SuggestionSpanComparator();
mSpansLengths = new HashMap<SuggestionSpan, Integer>();
}
@Override
@@ -2987,24 +3212,6 @@ public class Editor {
mIsShowingUp = false;
}
private final class SuggestionInfo {
int suggestionStart, suggestionEnd; // range of actual suggestion within text
// the SuggestionSpan that this TextView represents
@Nullable
SuggestionSpan suggestionSpan;
int suggestionIndex; // the index of this suggestion inside suggestionSpan
@Nullable
final SpannableStringBuilder text = new SpannableStringBuilder();
void clear() {
suggestionSpan = null;
text.clear();
}
}
private class SuggestionAdapter extends BaseAdapter {
private LayoutInflater mInflater = (LayoutInflater) mTextView.getContext().
getSystemService(Context.LAYOUT_INFLATER_SERVICE);
@@ -3034,55 +3241,11 @@ public class Editor {
}
final SuggestionInfo suggestionInfo = mSuggestionInfos[position];
textView.setText(suggestionInfo.text);
textView.setText(suggestionInfo.mText);
return textView;
}
}
private class SuggestionSpanComparator implements Comparator<SuggestionSpan> {
public int compare(SuggestionSpan span1, SuggestionSpan span2) {
final int flag1 = span1.getFlags();
final int flag2 = span2.getFlags();
if (flag1 != flag2) {
// The order here should match what is used in updateDrawState
final boolean easy1 = (flag1 & SuggestionSpan.FLAG_EASY_CORRECT) != 0;
final boolean easy2 = (flag2 & SuggestionSpan.FLAG_EASY_CORRECT) != 0;
final boolean misspelled1 = (flag1 & SuggestionSpan.FLAG_MISSPELLED) != 0;
final boolean misspelled2 = (flag2 & SuggestionSpan.FLAG_MISSPELLED) != 0;
if (easy1 && !misspelled1) return -1;
if (easy2 && !misspelled2) return 1;
if (misspelled1) return -1;
if (misspelled2) return 1;
}
return mSpansLengths.get(span1).intValue() - mSpansLengths.get(span2).intValue();
}
}
/**
* Returns the suggestion spans that cover the current cursor position. The suggestion
* spans are sorted according to the length of text that they are attached to.
*/
private SuggestionSpan[] getSuggestionSpans() {
int pos = mTextView.getSelectionStart();
Spannable spannable = (Spannable) mTextView.getText();
SuggestionSpan[] suggestionSpans = spannable.getSpans(pos, pos, SuggestionSpan.class);
mSpansLengths.clear();
for (SuggestionSpan suggestionSpan : suggestionSpans) {
int start = spannable.getSpanStart(suggestionSpan);
int end = spannable.getSpanEnd(suggestionSpan);
mSpansLengths.put(suggestionSpan, Integer.valueOf(end - start));
}
// The suggestions are sorted according to their types (easy correction first, then
// misspelled) and to the length of the text that they cover (shorter first).
Arrays.sort(suggestionSpans, mSuggestionSpanComparator);
mSpansLengths.clear();
return suggestionSpans;
}
@VisibleForTesting
public ViewGroup getContentViewForTesting() {
return mContentView;
@@ -3166,66 +3329,26 @@ public class Editor {
private boolean updateSuggestions() {
Spannable spannable = (Spannable) mTextView.getText();
SuggestionSpan[] suggestionSpans = getSuggestionSpans();
mNumberOfSuggestions =
mSuggestionHelper.getSuggestionInfo(mSuggestionInfos);
if (mNumberOfSuggestions == 0) {
return false;
}
final int nbSpans = suggestionSpans.length;
// Suggestions are shown after a delay: the underlying spans may have been removed
if (nbSpans == 0) return false;
mNumberOfSuggestions = 0;
int spanUnionStart = mTextView.getText().length();
int spanUnionEnd = 0;
mMisspelledSpan = null;
int underlineColor = 0;
for (int spanIndex = 0; spanIndex < nbSpans; spanIndex++) {
SuggestionSpan suggestionSpan = suggestionSpans[spanIndex];
final int spanStart = spannable.getSpanStart(suggestionSpan);
final int spanEnd = spannable.getSpanEnd(suggestionSpan);
spanUnionStart = Math.min(spanStart, spanUnionStart);
spanUnionEnd = Math.max(spanEnd, spanUnionEnd);
for (int i = 0; i < mNumberOfSuggestions; i++) {
final SuggestionInfo suggestionInfo = mSuggestionInfos[i];
final SuggestionSpan suggestionSpan = suggestionInfo.mSuggestionSpan;
if ((suggestionSpan.getFlags() & SuggestionSpan.FLAG_MISSPELLED) != 0) {
mMisspelledSpan = suggestionSpan;
}
// The first span dictates the background color of the highlighted text
if (spanIndex == 0) underlineColor = suggestionSpan.getUnderlineColor();
String[] suggestions = suggestionSpan.getSuggestions();
int nbSuggestions = suggestions.length;
for (int suggestionIndex = 0; suggestionIndex < nbSuggestions; suggestionIndex++) {
String suggestion = suggestions[suggestionIndex];
boolean suggestionIsDuplicate = false;
for (int i = 0; i < mNumberOfSuggestions; i++) {
if (mSuggestionInfos[i].text.toString().equals(suggestion)) {
SuggestionSpan otherSuggestionSpan = mSuggestionInfos[i].suggestionSpan;
final int otherSpanStart = spannable.getSpanStart(otherSuggestionSpan);
final int otherSpanEnd = spannable.getSpanEnd(otherSuggestionSpan);
if (spanStart == otherSpanStart && spanEnd == otherSpanEnd) {
suggestionIsDuplicate = true;
break;
}
}
}
if (!suggestionIsDuplicate) {
SuggestionInfo suggestionInfo = mSuggestionInfos[mNumberOfSuggestions];
suggestionInfo.suggestionSpan = suggestionSpan;
suggestionInfo.suggestionIndex = suggestionIndex;
suggestionInfo.text.replace(0, suggestionInfo.text.length(), suggestion);
mNumberOfSuggestions++;
if (mNumberOfSuggestions == MAX_NUMBER_SUGGESTIONS) {
// Also end outer for loop
spanIndex = nbSpans;
break;
}
}
}
final int spanStart = spannable.getSpanStart(suggestionSpan);
final int spanEnd = spannable.getSpanEnd(suggestionSpan);
spanUnionStart = Math.min(spanUnionStart, spanStart);
spanUnionEnd = Math.max(spanUnionEnd, spanEnd);
}
for (int i = 0; i < mNumberOfSuggestions; i++) {
@@ -3244,6 +3367,7 @@ public class Editor {
mAddToDictionaryButton.setVisibility(addToDictionaryButtonVisibility);
if (mSuggestionRangeSpan == null) mSuggestionRangeSpan = new SuggestionRangeSpan();
final int underlineColor = mSuggestionInfos[0].mSuggestionSpan.getUnderlineColor();
if (underlineColor == 0) {
// Fallback on the default highlight color when the first span does not provide one
mSuggestionRangeSpan.setBackgroundColor(mTextView.mHighlightColor);
@@ -3263,21 +3387,21 @@ public class Editor {
private void highlightTextDifferences(SuggestionInfo suggestionInfo, int unionStart,
int unionEnd) {
final Spannable text = (Spannable) mTextView.getText();
final int spanStart = text.getSpanStart(suggestionInfo.suggestionSpan);
final int spanEnd = text.getSpanEnd(suggestionInfo.suggestionSpan);
final int spanStart = text.getSpanStart(suggestionInfo.mSuggestionSpan);
final int spanEnd = text.getSpanEnd(suggestionInfo.mSuggestionSpan);
// Adjust the start/end of the suggestion span
suggestionInfo.suggestionStart = spanStart - unionStart;
suggestionInfo.suggestionEnd = suggestionInfo.suggestionStart
+ suggestionInfo.text.length();
suggestionInfo.mSuggestionStart = spanStart - unionStart;
suggestionInfo.mSuggestionEnd = suggestionInfo.mSuggestionStart
+ suggestionInfo.mText.length();
suggestionInfo.text.setSpan(mHighlightSpan, 0, suggestionInfo.text.length(),
suggestionInfo.mText.setSpan(mHighlightSpan, 0, suggestionInfo.mText.length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
// Add the text before and after the span.
final String textAsString = text.toString();
suggestionInfo.text.insert(0, textAsString.substring(unionStart, spanStart));
suggestionInfo.text.append(textAsString.substring(spanEnd, unionEnd));
suggestionInfo.mText.insert(0, textAsString.substring(unionStart, spanStart));
suggestionInfo.mText.append(textAsString.substring(spanEnd, unionEnd));
}
@Override
@@ -3285,69 +3409,14 @@ public class Editor {
Editable editable = (Editable) mTextView.getText();
SuggestionInfo suggestionInfo = mSuggestionInfos[position];
final int spanStart = editable.getSpanStart(suggestionInfo.suggestionSpan);
final int spanEnd = editable.getSpanEnd(suggestionInfo.suggestionSpan);
final int spanStart = editable.getSpanStart(suggestionInfo.mSuggestionSpan);
final int spanEnd = editable.getSpanEnd(suggestionInfo.mSuggestionSpan);
if (spanStart < 0 || spanEnd <= spanStart) {
// Span has been removed
hideWithCleanUp();
return;
}
final String originalText = TextUtils.substring(editable, spanStart, spanEnd);
// SuggestionSpans are removed by replace: save them before
final SuggestionSpan[] suggestionSpans = editable.getSpans(spanStart, spanEnd,
SuggestionSpan.class);
final int length = suggestionSpans.length;
final int[] suggestionSpansStarts = new int[length];
final int[] suggestionSpansEnds = new int[length];
final int[] suggestionSpansFlags = new int[length];
for (int i = 0; i < length; i++) {
final SuggestionSpan suggestionSpan = suggestionSpans[i];
suggestionSpansStarts[i] = editable.getSpanStart(suggestionSpan);
suggestionSpansEnds[i] = editable.getSpanEnd(suggestionSpan);
suggestionSpansFlags[i] = editable.getSpanFlags(suggestionSpan);
// Remove potential misspelled flags
int suggestionSpanFlags = suggestionSpan.getFlags();
if ((suggestionSpanFlags & SuggestionSpan.FLAG_MISSPELLED) > 0) {
suggestionSpanFlags &= ~SuggestionSpan.FLAG_MISSPELLED;
suggestionSpanFlags &= ~SuggestionSpan.FLAG_EASY_CORRECT;
suggestionSpan.setFlags(suggestionSpanFlags);
}
}
final int suggestionStart = suggestionInfo.suggestionStart;
final int suggestionEnd = suggestionInfo.suggestionEnd;
final String suggestion = suggestionInfo.text.subSequence(
suggestionStart, suggestionEnd).toString();
mTextView.replaceText_internal(spanStart, spanEnd, suggestion);
// Notify source IME of the suggestion pick. Do this before
// swaping texts.
suggestionInfo.suggestionSpan.notifySelection(
mTextView.getContext(), originalText, suggestionInfo.suggestionIndex);
// Swap text content between actual text and Suggestion span
final String[] suggestions = suggestionInfo.suggestionSpan.getSuggestions();
suggestions[suggestionInfo.suggestionIndex] = originalText;
// Restore previous SuggestionSpans
final int lengthDifference = suggestion.length() - (spanEnd - spanStart);
for (int i = 0; i < length; i++) {
// Only spans that include the modified region make sense after replacement
// Spans partially included in the replaced region are removed, there is no
// way to assign them a valid range after replacement
if (suggestionSpansStarts[i] <= spanStart &&
suggestionSpansEnds[i] >= spanEnd) {
mTextView.setSpan_internal(suggestionSpans[i], suggestionSpansStarts[i],
suggestionSpansEnds[i] + lengthDifference, suggestionSpansFlags[i]);
}
}
// Move cursor at the end of the replaced word
final int newCursorPosition = spanEnd + lengthDifference;
mTextView.setCursorPosition_internal(newCursorPosition, newCursorPosition);
replaceWithSuggestion(suggestionInfo, spanStart, spanEnd);
hideWithCleanUp();
}
}