Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 8a147ba6 authored by Seigo Nonaka's avatar Seigo Nonaka Committed by Android (Google) Code Review
Browse files

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

parents e6a67c28 66260c60
Loading
Loading
Loading
Loading
+6 −0
Original line number Diff line number Diff line
@@ -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 {
+351 −0
Original line number Diff line number Diff line
/*
 * 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;
    }
}
+101 −0
Original line number Diff line number Diff line
@@ -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;
    }
}
+2 −0
Original line number Diff line number Diff line
@@ -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. -->
+6 −0
Original line number Diff line number Diff line
@@ -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" />
Loading