Merge "Implement Ctrl+B/I/U shorcut"

This commit is contained in:
Seigo Nonaka
2022-09-09 04:19:47 +00:00
committed by Android (Google) Code Review
6 changed files with 470 additions and 0 deletions

View File

@@ -636,6 +636,7 @@ package android {
field public static final int ellipsize = 16842923; // 0x10100ab
field public static final int ems = 16843096; // 0x1010158
field public static final int enableOnBackInvokedCallback = 16844396; // 0x101066c
field public static final int enableTextStylingShortcuts;
field public static final int enableVrMode = 16844069; // 0x1010525
field public static final int enabled = 16842766; // 0x101000e
field public static final int end = 16843996; // 0x10104dc
@@ -2113,6 +2114,7 @@ package android {
field public static final int addToDictionary = 16908330; // 0x102002a
field public static final int autofill = 16908355; // 0x1020043
field public static final int background = 16908288; // 0x1020000
field public static final int bold;
field public static final int button1 = 16908313; // 0x1020019
field public static final int button2 = 16908314; // 0x102001a
field public static final int button3 = 16908315; // 0x102001b
@@ -2138,6 +2140,7 @@ package android {
field public static final int inputExtractAccessories = 16908378; // 0x102005a
field public static final int inputExtractAction = 16908377; // 0x1020059
field public static final int inputExtractEditText = 16908325; // 0x1020025
field public static final int italic;
field @Deprecated public static final int keyboardView = 16908326; // 0x1020026
field public static final int list = 16908298; // 0x102000a
field public static final int list_container = 16908351; // 0x102003f
@@ -2169,6 +2172,7 @@ package android {
field public static final int textAssist = 16908353; // 0x1020041
field public static final int title = 16908310; // 0x1020016
field public static final int toggle = 16908311; // 0x1020017
field public static final int underline;
field public static final int undo = 16908338; // 0x1020032
field public static final int widget_frame = 16908312; // 0x1020018
}
@@ -55995,9 +55999,11 @@ package android.widget {
ctor public EditText(android.content.Context, android.util.AttributeSet, int, int);
method public void extendSelection(int);
method public android.text.Editable getText();
method public boolean isStyleShortcutEnabled();
method public void selectAll();
method public void setSelection(int, int);
method public void setSelection(int);
method public void setStyleShortcutsEnabled(boolean);
}
public interface ExpandableListAdapter {

View File

@@ -0,0 +1,351 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.text.style;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.graphics.Typeface;
import android.text.Spannable;
import android.text.Spanned;
import android.util.LongArray;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* @hide
*/
public class SpanUtils {
private SpanUtils() {} // Do not instantiate
/**
* Toggle the bold state of the given range.
*
* If there is at least one character is not bold in the given range, make the entire region to
* be bold. If all characters of the given range is already bolded, this method removes bold
* style from the given selection.
*
* @param spannable a spannable string
* @param min minimum inclusive index of the selection.
* @param max maximum exclusive index of the selection.
* @return true if the selected region is toggled.
*/
public static boolean toggleBold(@NonNull Spannable spannable,
@IntRange(from = 0) int min, @IntRange(from = 0) int max) {
if (min == max) {
return false;
}
final StyleSpan[] boldSpans = spannable.getSpans(min, max, StyleSpan.class);
final ArrayList<StyleSpan> filteredBoldSpans = new ArrayList<>();
for (StyleSpan span : boldSpans) {
if ((span.getStyle() & Typeface.BOLD) == Typeface.BOLD) {
filteredBoldSpans.add(span);
}
}
if (!isCovered(spannable, filteredBoldSpans, min, max)) {
// At least one character doesn't have bold style. Making given region bold.
spannable.setSpan(
new StyleSpan(Typeface.BOLD), min, max, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
return true;
}
// Span covers the entire selection. Removing spans from tha region.
for (int si = 0; si < filteredBoldSpans.size(); ++si) {
final StyleSpan span = filteredBoldSpans.get(si);
final int start = spannable.getSpanStart(span);
final int end = spannable.getSpanEnd(span);
final int flag = spannable.getSpanFlags(span);
// If BOLD_ITALIC style is attached, need to set ITALIC span to the subtracted range.
final boolean needItalicSpan = (span.getStyle() & Typeface.ITALIC) == Typeface.ITALIC;
if (start < min) {
if (end > max) {
// selection: ------------|===================|----------------
// span: <-------------------------------->
// result: <-------> <---->
spannable.setSpan(span, start, min, flag);
spannable.setSpan(new StyleSpan(span.getStyle()), max, end, flag);
if (needItalicSpan) {
spannable.setSpan(new StyleSpan(Typeface.ITALIC), min, max, flag);
}
} else {
// selection: ------------|===================|----------------
// span: <----------->
// result: <------->
spannable.setSpan(span, start, min, flag);
if (needItalicSpan) {
spannable.setSpan(new StyleSpan(Typeface.ITALIC), min, end, flag);
}
}
} else {
if (end > max) {
// selection: ------------|===================|----------------
// span: <------------------------>
// result: <------------>
spannable.setSpan(span, max, end, flag);
if (needItalicSpan) {
spannable.setSpan(new StyleSpan(Typeface.ITALIC), max, end, flag);
}
} else {
// selection: ------------|===================|----------------
// span: <----------->
// result:
spannable.removeSpan(span);
if (needItalicSpan) {
spannable.setSpan(new StyleSpan(Typeface.ITALIC), start, end, flag);
}
}
}
}
return true;
}
/**
* Toggle the italic state of the given range.
*
* If there is at least one character is not italic in the given range, make the entire region
* to be italic. If all characters of the given range is already italic, this method removes
* italic style from the given selection.
*
* @param spannable a spannable string
* @param min minimum inclusive index of the selection.
* @param max maximum exclusive index of the selection.
* @return true if the selected region is toggled.
*/
public static boolean toggleItalic(@NonNull Spannable spannable,
@IntRange(from = 0) int min, @IntRange(from = 0) int max) {
if (min == max) {
return false;
}
final StyleSpan[] boldSpans = spannable.getSpans(min, max, StyleSpan.class);
final ArrayList<StyleSpan> filteredBoldSpans = new ArrayList<>();
for (StyleSpan span : boldSpans) {
if ((span.getStyle() & Typeface.ITALIC) == Typeface.ITALIC) {
filteredBoldSpans.add(span);
}
}
if (!isCovered(spannable, filteredBoldSpans, min, max)) {
// At least one character doesn't have italic style. Making given region italic.
spannable.setSpan(
new StyleSpan(Typeface.ITALIC), min, max, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
return true;
}
// Span covers the entire selection. Removing spans from tha region.
for (int si = 0; si < filteredBoldSpans.size(); ++si) {
final StyleSpan span = filteredBoldSpans.get(si);
final int start = spannable.getSpanStart(span);
final int end = spannable.getSpanEnd(span);
final int flag = spannable.getSpanFlags(span);
// If BOLD_ITALIC style is attached, need to set BOLD span to the subtracted range.
final boolean needBoldSpan = (span.getStyle() & Typeface.BOLD) == Typeface.BOLD;
if (start < min) {
if (end > max) {
// selection: ------------|===================|----------------
// span: <-------------------------------->
// result: <-------> <---->
spannable.setSpan(span, start, min, flag);
spannable.setSpan(new StyleSpan(span.getStyle()), max, end, flag);
if (needBoldSpan) {
spannable.setSpan(new StyleSpan(Typeface.BOLD), min, max, flag);
}
} else {
// selection: ------------|===================|----------------
// span: <----------->
// result: <------->
spannable.setSpan(span, start, min, flag);
if (needBoldSpan) {
spannable.setSpan(new StyleSpan(Typeface.BOLD), min, end, flag);
}
}
} else {
if (end > max) {
// selection: ------------|===================|----------------
// span: <------------------------>
// result: <------------>
spannable.setSpan(span, max, end, flag);
if (needBoldSpan) {
spannable.setSpan(new StyleSpan(Typeface.BOLD), max, end, flag);
}
} else {
// selection: ------------|===================|----------------
// span: <----------->
// result:
spannable.removeSpan(span);
if (needBoldSpan) {
spannable.setSpan(new StyleSpan(Typeface.BOLD), start, end, flag);
}
}
}
}
return true;
}
/**
* Toggle the underline state of the given range.
*
* If there is at least one character is not underlined in the given range, make the entire
* region to underlined. If all characters of the given range is already underlined, this
* method removes underline from the given selection.
*
* @param spannable a spannable string
* @param min minimum inclusive index of the selection.
* @param max maximum exclusive index of the selection.
* @return true if the selected region is toggled.
*/
public static boolean toggleUnderline(@NonNull Spannable spannable,
@IntRange(from = 0) int min, @IntRange(from = 0) int max) {
if (min == max) {
return false;
}
final List<UnderlineSpan> spans =
Arrays.asList(spannable.getSpans(min, max, UnderlineSpan.class));
if (!isCovered(spannable, spans, min, max)) {
// At least one character doesn't have underline style. Making given region underline.
spannable.setSpan(new UnderlineSpan(), min, max, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
return true;
}
// Span covers the entire selection. Removing spans from tha region.
for (int si = 0; si < spans.size(); ++si) {
final UnderlineSpan span = spans.get(si);
final int start = spannable.getSpanStart(span);
final int end = spannable.getSpanEnd(span);
final int flag = spannable.getSpanFlags(span);
if (start < min) {
if (end > max) {
// selection: ------------|===================|----------------
// span: <-------------------------------->
// result: <-------> <---->
spannable.setSpan(span, start, min, flag);
spannable.setSpan(new UnderlineSpan(), max, end, flag);
} else {
// selection: ------------|===================|----------------
// span: <----------->
// result: <------->
spannable.setSpan(span, start, min, flag);
}
} else {
if (end > max) {
// selection: ------------|===================|----------------
// span: <------------------------>
// result: <------------>
spannable.setSpan(span, max, end, flag);
} else {
// selection: ------------|===================|----------------
// span: <----------->
// result:
spannable.removeSpan(span);
}
}
}
return true;
}
private static long pack(int from, int to) {
return ((long) from) << 32 | (long) to;
}
private static int min(long packed) {
return (int) (packed >> 32);
}
private static int max(long packed) {
return (int) (packed & 0xFFFFFFFFL);
}
private static boolean hasIntersection(int aMin, int aMax, int bMin, int bMax) {
return aMin < bMax && bMin < aMax;
}
private static long intersection(int aMin, int aMax, int bMin, int bMax) {
return pack(Math.max(aMin, bMin), Math.min(aMax, bMax));
}
private static <T> boolean isCovered(@NonNull Spannable spannable, @NonNull List<T> spans,
@IntRange(from = 0) int min, @IntRange(from = 0) int max) {
if (min == max) {
return false;
}
LongArray uncoveredRanges = new LongArray();
LongArray nextUncoveredRanges = new LongArray();
uncoveredRanges.add(pack(min, max));
for (int si = 0; si < spans.size(); ++si) {
final T span = spans.get(si);
final int start = spannable.getSpanStart(span);
final int end = spannable.getSpanEnd(span);
for (int i = 0; i < uncoveredRanges.size(); ++i) {
final long packed = uncoveredRanges.get(i);
final int uncoveredStart = min(packed);
final int uncoveredEnd = max(packed);
if (!hasIntersection(start, end, uncoveredStart, uncoveredEnd)) {
// This span doesn't affect this uncovered range. Try next span.
nextUncoveredRanges.add(packed);
} else {
// This span has an intersection with uncovered range. Update the uncovered
// range.
long intersectionPack = intersection(start, end, uncoveredStart, uncoveredEnd);
int intersectStart = min(intersectionPack);
int intersectEnd = max(intersectionPack);
// Uncovered Range : ----------|=======================|-------------
// Intersection : <---------->
// Remaining uncovered ranges: ----------|=====|----------|======|-------------
if (uncoveredStart != intersectStart) {
// There is still uncovered area on the left.
nextUncoveredRanges.add(pack(uncoveredStart, intersectStart));
}
if (intersectEnd != uncoveredEnd) {
// There is still uncovered area on the right.
nextUncoveredRanges.add(pack(intersectEnd, uncoveredEnd));
}
}
}
if (nextUncoveredRanges.size() == 0) {
return true;
}
// Swap the uncoveredRanges and nextUncoveredRanges and clear the next one.
final LongArray tmp = nextUncoveredRanges;
nextUncoveredRanges = uncoveredRanges;
uncoveredRanges = tmp;
nextUncoveredRanges.clear();
}
return false;
}
}

View File

@@ -17,13 +17,17 @@
package android.widget;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.text.Editable;
import android.text.Selection;
import android.text.Spannable;
import android.text.TextUtils;
import android.text.method.ArrowKeyMovementMethod;
import android.text.method.MovementMethod;
import android.text.style.SpanUtils;
import android.util.AttributeSet;
import android.view.KeyEvent;
/*
* This is supposed to be a *very* thin veneer over TextView.
@@ -69,8 +73,18 @@ import android.util.AttributeSet;
* See {@link android.R.styleable#EditText EditText Attributes},
* {@link android.R.styleable#TextView TextView Attributes},
* {@link android.R.styleable#View View Attributes}
*
* @attr ref android.R.styleable#EditText_enableTextStylingShortcuts
*/
public class EditText extends TextView {
// True if the style shortcut is enabled.
private boolean mStyleShortcutsEnabled = false;
private static final int ID_BOLD = android.R.id.bold;
private static final int ID_ITALIC = android.R.id.italic;
private static final int ID_UNDERLINE = android.R.id.underline;
public EditText(Context context) {
this(context, null);
}
@@ -85,6 +99,20 @@ public class EditText extends TextView {
public EditText(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
final Resources.Theme theme = context.getTheme();
final TypedArray a = theme.obtainStyledAttributes(attrs,
com.android.internal.R.styleable.EditText, defStyleAttr, defStyleRes);
final int n = a.getIndexCount();
for (int i = 0; i < n; ++i) {
int attr = a.getIndex(i);
switch (attr) {
case com.android.internal.R.styleable.EditText_enableTextStylingShortcuts:
mStyleShortcutsEnabled = a.getBoolean(attr, false);
break;
}
}
}
@Override
@@ -178,4 +206,77 @@ public class EditText extends TextView {
protected boolean supportsAutoSizeText() {
return false;
}
@Override
public boolean onKeyShortcut(int keyCode, KeyEvent event) {
if (event.hasModifiers(KeyEvent.META_CTRL_ON)) {
// Handle Ctrl-only shortcuts.
switch (keyCode) {
case KeyEvent.KEYCODE_B:
if (mStyleShortcutsEnabled && hasSelection()) {
return onTextContextMenuItem(ID_BOLD);
}
break;
case KeyEvent.KEYCODE_I:
if (mStyleShortcutsEnabled && hasSelection()) {
return onTextContextMenuItem(ID_ITALIC);
}
break;
case KeyEvent.KEYCODE_U:
if (mStyleShortcutsEnabled && hasSelection()) {
return onTextContextMenuItem(ID_UNDERLINE);
}
break;
}
}
return super.onKeyShortcut(keyCode, event);
}
@Override
public boolean onTextContextMenuItem(int id) {
// TODO: Move to switch-case once the resource ID is finalized.
if (id == ID_BOLD || id == ID_ITALIC || id == ID_UNDERLINE) {
return performStylingAction(id);
}
return super.onTextContextMenuItem(id);
}
private boolean performStylingAction(int actionId) {
final int selectionStart = getSelectionStart();
final int selectionEnd = getSelectionEnd();
if (selectionStart < 0 || selectionEnd < 0) {
return false; // There is no selection.
}
int min = Math.min(selectionStart, selectionEnd);
int max = Math.max(selectionStart, selectionEnd);
Spannable spannable = getText();
if (actionId == ID_BOLD) {
return SpanUtils.toggleBold(spannable, min, max);
} else if (actionId == ID_ITALIC) {
return SpanUtils.toggleItalic(spannable, min, max);
} else if (actionId == ID_UNDERLINE) {
return SpanUtils.toggleUnderline(spannable, min, max);
}
return false;
}
/**
* Enables styls shortcuts, e.g. Ctrl+B for making text bold.
*
* @param enabled true for enabled, false for disabled.
*/
public void setStyleShortcutsEnabled(boolean enabled) {
mStyleShortcutsEnabled = enabled;
}
/**
* Return true if style shortcut is enabled, otherwise returns false.
* @return true if style shortcut is enabled, otherwise returns false.
*/
public boolean isStyleShortcutEnabled() {
return mStyleShortcutsEnabled;
}
}

View File

@@ -4509,6 +4509,8 @@
</attr>
</declare-styleable>
<declare-styleable name="EditText">
<!-- Enables styling shortcuts, e.g. Ctrl+B for bold. This is off by default. -->
<attr name="enableTextStylingShortcuts" format="boolean" />
</declare-styleable>
<declare-styleable name="FastScroll">
<!-- Drawable used for the scroll bar thumb. -->

View File

@@ -70,6 +70,12 @@
<item type="id" name="cut" />
<item type="id" name="copy" />
<item type="id" name="paste" />
<!-- Editor action that makes selected text bold. -->
<item type="id" name="bold" />
<!-- Editor action that makes selected text italic. -->
<item type="id" name="italic" />
<!-- Editor action that makes selected text underline. -->
<item type="id" name="underline" />
<item type="id" name="copyUrl" />
<item type="id" name="selectTextMode" />
<item type="id" name="switchInputMethod" />

View File

@@ -115,9 +115,13 @@
<public name="handwritingBoundsOffsetRight" />
<public name="handwritingBoundsOffsetBottom" />
<public name="accessibilityDataPrivate" />
<public name="enableTextStylingShortcuts" />
</staging-public-group>
<staging-public-group type="id" first-id="0x01cd0000">
<public name="bold" />
<public name="italic" />
<public name="underline" />
</staging-public-group>
<staging-public-group type="style" first-id="0x01cc0000">