diff options
Diffstat (limited to 'core/java/android/text')
95 files changed, 17407 insertions, 0 deletions
diff --git a/core/java/android/text/AlteredCharSequence.java b/core/java/android/text/AlteredCharSequence.java new file mode 100644 index 0000000..4cc71fd --- /dev/null +++ b/core/java/android/text/AlteredCharSequence.java @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2006 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; + +// XXX should this really be in the public API at all? +/** + * An AlteredCharSequence is a CharSequence that is largely mirrored from + * another CharSequence, except that a specified range of characters are + * mirrored from a different char array instead. + */ +public class AlteredCharSequence +implements CharSequence, GetChars +{ + /** + * Create an AlteredCharSequence whose text (and possibly spans) + * are mirrored from <code>source</code>, except that the range of + * offsets <code>substart</code> inclusive to <code>subend</code> exclusive + * are mirrored instead from <code>sub</code>, beginning at offset 0. + */ + public static AlteredCharSequence make(CharSequence source, char[] sub, + int substart, int subend) { + if (source instanceof Spanned) + return new AlteredSpanned(source, sub, substart, subend); + else + return new AlteredCharSequence(source, sub, substart, subend); + } + + private AlteredCharSequence(CharSequence source, char[] sub, + int substart, int subend) { + mSource = source; + mChars = sub; + mStart = substart; + mEnd = subend; + } + + /* package */ void update(char[] sub, int substart, int subend) { + mChars = sub; + mStart = substart; + mEnd = subend; + } + + private static class AlteredSpanned + extends AlteredCharSequence + implements Spanned + { + private AlteredSpanned(CharSequence source, char[] sub, + int substart, int subend) { + super(source, sub, substart, subend); + mSpanned = (Spanned) source; + } + + public <T> T[] getSpans(int start, int end, Class<T> kind) { + return mSpanned.getSpans(start, end, kind); + } + + public int getSpanStart(Object span) { + return mSpanned.getSpanStart(span); + } + + public int getSpanEnd(Object span) { + return mSpanned.getSpanEnd(span); + } + + public int getSpanFlags(Object span) { + return mSpanned.getSpanFlags(span); + } + + public int nextSpanTransition(int start, int end, Class kind) { + return mSpanned.nextSpanTransition(start, end, kind); + } + + private Spanned mSpanned; + } + + public char charAt(int off) { + if (off >= mStart && off < mEnd) + return mChars[off - mStart]; + else + return mSource.charAt(off); + } + + public int length() { + return mSource.length(); + } + + public CharSequence subSequence(int start, int end) { + return AlteredCharSequence.make(mSource.subSequence(start, end), + mChars, mStart - start, mEnd - start); + } + + public void getChars(int start, int end, char[] dest, int off) { + TextUtils.getChars(mSource, start, end, dest, off); + + start = Math.max(mStart, start); + end = Math.min(mEnd, end); + + if (start > end) + System.arraycopy(mChars, start - mStart, dest, off, end - start); + } + + public String toString() { + int len = length(); + + char[] ret = new char[len]; + getChars(0, len, ret, 0); + return String.valueOf(ret); + } + + private int mStart; + private int mEnd; + private char[] mChars; + private CharSequence mSource; +} diff --git a/core/java/android/text/AndroidCharacter.java b/core/java/android/text/AndroidCharacter.java new file mode 100644 index 0000000..6dfd64d --- /dev/null +++ b/core/java/android/text/AndroidCharacter.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2006 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; + +/** + * AndroidCharacter exposes some character properties that are not + * easily accessed from java.lang.Character. + */ +public class AndroidCharacter +{ + /** + * Fill in the first <code>count</code> bytes of <code>dest</code> with the + * directionalities from the first <code>count</code> chars of <code>src</code>. + * This is just like Character.getDirectionality() except it is a + * batch operation. + */ + public native static void getDirectionalities(char[] src, byte[] dest, + int count); + /** + * Replace the specified slice of <code>text</code> with the chars' + * right-to-left mirrors (if any), returning true if any + * replacements were made. + */ + public native static boolean mirror(char[] text, int start, int count); + + /** + * Return the right-to-left mirror (or the original char if none) + * of the specified char. + */ + public native static char getMirror(char ch); +} diff --git a/core/java/android/text/Annotation.java b/core/java/android/text/Annotation.java new file mode 100644 index 0000000..a3812a8 --- /dev/null +++ b/core/java/android/text/Annotation.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2008 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; + +/** + * Annotations are simple key-value pairs that are preserved across + * TextView save/restore cycles and can be used to keep application-specific + * data that needs to be maintained for regions of text. + */ +public class Annotation { + private String mKey; + private String mValue; + + public Annotation(String key, String value) { + mKey = key; + mValue = value; + } + + public String getKey() { + return mKey; + } + + public String getValue() { + return mValue; + } +} diff --git a/core/java/android/text/AutoText.java b/core/java/android/text/AutoText.java new file mode 100644 index 0000000..508d740 --- /dev/null +++ b/core/java/android/text/AutoText.java @@ -0,0 +1,246 @@ +/* + * Copyright (C) 2007 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; + +import android.content.res.Resources; +import android.content.res.XmlResourceParser; +import com.android.internal.util.XmlUtils; +import android.view.View; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.util.Locale; + +/** + * This class accesses a dictionary of corrections to frequent misspellings. + */ +public class AutoText { + // struct trie { + // char c; + // int off; + // struct trie *child; + // struct trie *next; + // }; + + private static final int TRIE_C = 0; + private static final int TRIE_OFF = 1; + private static final int TRIE_CHILD = 2; + private static final int TRIE_NEXT = 3; + + private static final int TRIE_SIZEOF = 4; + private static final char TRIE_NULL = (char) -1; + private static final int TRIE_ROOT = 0; + + private static final int INCREMENT = 1024; + + private static final int DEFAULT = 14337; // Size of the Trie 13 Aug 2007 + + private static final int RIGHT = 9300; // Size of 'right' 13 Aug 2007 + + private static AutoText sInstance = new AutoText(Resources.getSystem()); + private static Object sLock = new Object(); + + // TODO: + // + // Note the assumption that the destination strings total less than + // 64K characters and that the trie for the source side totals less + // than 64K chars/offsets/child pointers/next pointers. + // + // This seems very safe for English (currently 7K of destination, + // 14K of trie) but may need to be revisited. + + private char[] mTrie; + private char mTrieUsed; + private String mText; + private Locale mLocale; + + private AutoText(Resources resources) { + mLocale = resources.getConfiguration().locale; + init(resources); + } + + /** + * Retrieves a possible spelling correction for the specified range + * of text. Returns null if no correction can be found. + * The View is used to get the current Locale and Resources. + */ + public static String get(CharSequence src, final int start, final int end, + View view) { + Resources res = view.getContext().getResources(); + Locale locale = res.getConfiguration().locale; + AutoText instance; + + synchronized (sLock) { + instance = sInstance; + + if (!locale.equals(instance.mLocale)) { + instance = new AutoText(res); + sInstance = instance; + } + } + + return instance.lookup(src, start, end); + } + + private String lookup(CharSequence src, final int start, final int end) { + int here = mTrie[TRIE_ROOT]; + + for (int i = start; i < end; i++) { + char c = src.charAt(i); + + for (; here != TRIE_NULL; here = mTrie[here + TRIE_NEXT]) { + if (c == mTrie[here + TRIE_C]) { + if ((i == end - 1) + && (mTrie[here + TRIE_OFF] != TRIE_NULL)) { + int off = mTrie[here + TRIE_OFF]; + int len = mText.charAt(off); + + return mText.substring(off + 1, off + 1 + len); + } + + here = mTrie[here + TRIE_CHILD]; + break; + } + } + + if (here == TRIE_NULL) { + return null; + } + } + + return null; + } + + private void init(Resources r) { + XmlResourceParser parser = r.getXml(com.android.internal.R.xml.autotext); + + StringBuilder right = new StringBuilder(RIGHT); + mTrie = new char[DEFAULT]; + mTrie[TRIE_ROOT] = TRIE_NULL; + mTrieUsed = TRIE_ROOT + 1; + + try { + XmlUtils.beginDocument(parser, "words"); + String odest = ""; + char ooff = 0; + + while (true) { + XmlUtils.nextElement(parser); + + String element = parser.getName(); + if (element == null || !(element.equals("word"))) { + break; + } + + String src = parser.getAttributeValue(null, "src"); + if (parser.next() == XmlPullParser.TEXT) { + String dest = parser.getText(); + char off; + + if (dest.equals(odest)) { + off = ooff; + } else { + off = (char) right.length(); + right.append((char) dest.length()); + right.append(dest); + } + + add(src, off); + } + } + + // Don't let Resources cache a copy of all these strings. + r.flushLayoutCache(); + } catch (XmlPullParserException e) { + throw new RuntimeException(e); + } catch (IOException e) { + throw new RuntimeException(e); + } finally { + parser.close(); + } + + mText = right.toString(); + } + + private void add(String src, char off) { + int slen = src.length(); + int herep = TRIE_ROOT; + + for (int i = 0; i < slen; i++) { + char c = src.charAt(i); + boolean found = false; + + for (; mTrie[herep] != TRIE_NULL; + herep = mTrie[herep] + TRIE_NEXT) { + if (c == mTrie[mTrie[herep] + TRIE_C]) { + // There is a node for this letter, and this is the + // end, so fill in the right hand side fields. + + if (i == slen - 1) { + mTrie[mTrie[herep] + TRIE_OFF] = off; + return; + } + + // There is a node for this letter, and we need + // to go deeper into it to fill in the rest. + + herep = mTrie[herep] + TRIE_CHILD; + found = true; + break; + } + } + + if (!found) { + // No node for this letter yet. Make one. + + char node = newTrieNode(); + mTrie[herep] = node; + + mTrie[mTrie[herep] + TRIE_C] = c; + mTrie[mTrie[herep] + TRIE_OFF] = TRIE_NULL; + mTrie[mTrie[herep] + TRIE_NEXT] = TRIE_NULL; + mTrie[mTrie[herep] + TRIE_CHILD] = TRIE_NULL; + + // If this is the end of the word, fill in the offset. + + if (i == slen - 1) { + mTrie[mTrie[herep] + TRIE_OFF] = off; + return; + } + + // Otherwise, step in deeper and go to the next letter. + + herep = mTrie[herep] + TRIE_CHILD; + } + } + } + + private char newTrieNode() { + if (mTrieUsed + TRIE_SIZEOF > mTrie.length) { + char[] copy = new char[mTrie.length + INCREMENT]; + System.arraycopy(mTrie, 0, copy, 0, mTrie.length); + mTrie = copy; + } + + char ret = mTrieUsed; + mTrieUsed += TRIE_SIZEOF; + + return ret; + } +} diff --git a/core/java/android/text/BoringLayout.java b/core/java/android/text/BoringLayout.java new file mode 100644 index 0000000..2ee4f62 --- /dev/null +++ b/core/java/android/text/BoringLayout.java @@ -0,0 +1,388 @@ +/* + * Copyright (C) 2006 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; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Path; +import android.util.FloatMath; + +/** + * A BoringLayout is a very simple Layout implementation for text that + * fits on a single line and is all left-to-right characters. + * You will probably never want to make one of these yourself; + * if you do, be sure to call {@link #isBoring} first to make sure + * the text meets the criteria. + * <p>This class is used by widgets to control text layout. You should not need + * to use this class directly unless you are implementing your own widget + * or custom display object, in which case + * you are encouraged to use a Layout instead of calling + * {@link android.graphics.Canvas#drawText(java.lang.CharSequence, int, int, float, float, android.graphics.Paint) + * Canvas.drawText()} directly.</p> + */ +public class BoringLayout extends Layout implements TextUtils.EllipsizeCallback { + public static BoringLayout make(CharSequence source, + TextPaint paint, int outerwidth, + Alignment align, + float spacingmult, float spacingadd, + BoringLayout.Metrics metrics, boolean includepad) { + return new BoringLayout(source, paint, outerwidth, align, + spacingmult, spacingadd, metrics, + includepad); + } + + public static BoringLayout make(CharSequence source, + TextPaint paint, int outerwidth, + Alignment align, + float spacingmult, float spacingadd, + BoringLayout.Metrics metrics, boolean includepad, + TextUtils.TruncateAt ellipsize, int ellipsizedWidth) { + return new BoringLayout(source, paint, outerwidth, align, + spacingmult, spacingadd, metrics, + includepad, ellipsize, ellipsizedWidth); + } + + /** + * Returns a BoringLayout for the specified text, potentially reusing + * this one if it is already suitable. The caller must make sure that + * no one is still using this Layout. + */ + public BoringLayout replaceOrMake(CharSequence source, TextPaint paint, + int outerwidth, Alignment align, + float spacingmult, float spacingadd, + BoringLayout.Metrics metrics, + boolean includepad) { + replaceWith(source, paint, outerwidth, align, spacingmult, + spacingadd); + + mEllipsizedWidth = outerwidth; + mEllipsizedStart = 0; + mEllipsizedCount = 0; + + init(source, paint, outerwidth, align, spacingmult, spacingadd, + metrics, includepad, true); + return this; + } + + /** + * Returns a BoringLayout for the specified text, potentially reusing + * this one if it is already suitable. The caller must make sure that + * no one is still using this Layout. + */ + public BoringLayout replaceOrMake(CharSequence source, TextPaint paint, + int outerwidth, Alignment align, + float spacingmult, float spacingadd, + BoringLayout.Metrics metrics, + boolean includepad, + TextUtils.TruncateAt ellipsize, + int ellipsizedWidth) { + boolean trust; + + if (ellipsize == null) { + replaceWith(source, paint, outerwidth, align, spacingmult, + spacingadd); + + mEllipsizedWidth = outerwidth; + mEllipsizedStart = 0; + mEllipsizedCount = 0; + trust = true; + } else { + replaceWith(TextUtils.ellipsize(source, paint, ellipsizedWidth, + ellipsize, true, this), + paint, outerwidth, align, spacingmult, + spacingadd); + + mEllipsizedWidth = ellipsizedWidth; + trust = false; + } + + init(getText(), paint, outerwidth, align, spacingmult, spacingadd, + metrics, includepad, trust); + return this; + } + + public BoringLayout(CharSequence source, + TextPaint paint, int outerwidth, + Alignment align, + float spacingmult, float spacingadd, + BoringLayout.Metrics metrics, boolean includepad) { + super(source, paint, outerwidth, align, spacingmult, spacingadd); + + mEllipsizedWidth = outerwidth; + mEllipsizedStart = 0; + mEllipsizedCount = 0; + + init(source, paint, outerwidth, align, spacingmult, spacingadd, + metrics, includepad, true); + } + + public BoringLayout(CharSequence source, + TextPaint paint, int outerwidth, + Alignment align, + float spacingmult, float spacingadd, + BoringLayout.Metrics metrics, boolean includepad, + TextUtils.TruncateAt ellipsize, int ellipsizedWidth) { + /* + * It is silly to have to call super() and then replaceWith(), + * but we can't use "this" for the callback until the call to + * super() finishes. + */ + super(source, paint, outerwidth, align, spacingmult, spacingadd); + + boolean trust; + + if (ellipsize == null) { + mEllipsizedWidth = outerwidth; + mEllipsizedStart = 0; + mEllipsizedCount = 0; + trust = true; + } else { + replaceWith(TextUtils.ellipsize(source, paint, ellipsizedWidth, + ellipsize, true, this), + paint, outerwidth, align, spacingmult, + spacingadd); + + + mEllipsizedWidth = ellipsizedWidth; + trust = false; + } + + init(getText(), paint, outerwidth, align, spacingmult, spacingadd, + metrics, includepad, trust); + } + + /* package */ void init(CharSequence source, + TextPaint paint, int outerwidth, + Alignment align, + float spacingmult, float spacingadd, + BoringLayout.Metrics metrics, boolean includepad, + boolean trustWidth) { + int spacing; + + if (source instanceof String && align == Layout.Alignment.ALIGN_NORMAL) { + mDirect = source.toString(); + } else { + mDirect = null; + } + + mPaint = paint; + + if (includepad) { + spacing = metrics.bottom - metrics.top; + } else { + spacing = metrics.descent - metrics.ascent; + } + + if (spacingmult != 1 || spacingadd != 0) { + spacing = (int)(spacing * spacingmult + spacingadd + 0.5f); + } + + mBottom = spacing; + + if (includepad) { + mDesc = spacing + metrics.top; + } else { + mDesc = spacing + metrics.ascent; + } + + if (trustWidth) { + mMax = metrics.width; + } else { + /* + * If we have ellipsized, we have to actually calculate the + * width because the width that was passed in was for the + * full text, not the ellipsized form. + */ + synchronized (sTemp) { + mMax = (int) (FloatMath.ceil(Styled.measureText(paint, sTemp, + source, 0, source.length(), + null))); + } + } + + if (includepad) { + mTopPadding = metrics.top - metrics.ascent; + mBottomPadding = metrics.bottom - metrics.descent; + } + } + + /** + * Returns null if not boring; the width, ascent, and descent if boring. + */ + public static Metrics isBoring(CharSequence text, + TextPaint paint) { + return isBoring(text, paint, null); + } + + /** + * Returns null if not boring; the width, ascent, and descent in the + * provided Metrics object (or a new one if the provided one was null) + * if boring. + */ + public static Metrics isBoring(CharSequence text, TextPaint paint, + Metrics metrics) { + char[] temp = TextUtils.obtain(500); + int len = text.length(); + boolean boring = true; + + outer: + for (int i = 0; i < len; i += 500) { + int j = i + 500; + + if (j > len) + j = len; + + TextUtils.getChars(text, i, j, temp, 0); + + int n = j - i; + + for (int a = 0; a < n; a++) { + char c = temp[a]; + + if (c == '\n' || c == '\t' || c >= FIRST_RIGHT_TO_LEFT) { + boring = false; + break outer; + } + } + } + + TextUtils.recycle(temp); + + if (boring) { + Metrics fm = metrics; + if (fm == null) { + fm = new Metrics(); + } + + int wid; + + synchronized (sTemp) { + wid = (int) (FloatMath.ceil(Styled.measureText(paint, sTemp, + text, 0, text.length(), fm))); + } + fm.width = wid; + return fm; + } else { + return null; + } + } + + @Override public int getHeight() { + return mBottom; + } + + @Override public int getLineCount() { + return 1; + } + + @Override public int getLineTop(int line) { + if (line == 0) + return 0; + else + return mBottom; + } + + @Override public int getLineDescent(int line) { + return mDesc; + } + + @Override public int getLineStart(int line) { + if (line == 0) + return 0; + else + return getText().length(); + } + + @Override public int getParagraphDirection(int line) { + return DIR_LEFT_TO_RIGHT; + } + + @Override public boolean getLineContainsTab(int line) { + return false; + } + + @Override public float getLineMax(int line) { + return mMax; + } + + @Override public final Directions getLineDirections(int line) { + return Layout.DIRS_ALL_LEFT_TO_RIGHT; + } + + public int getTopPadding() { + return mTopPadding; + } + + public int getBottomPadding() { + return mBottomPadding; + } + + @Override + public int getEllipsisCount(int line) { + return mEllipsizedCount; + } + + @Override + public int getEllipsisStart(int line) { + return mEllipsizedStart; + } + + @Override + public int getEllipsizedWidth() { + return mEllipsizedWidth; + } + + // Override draw so it will be faster. + @Override + public void draw(Canvas c, Path highlight, Paint highlightpaint, + int cursorOffset) { + if (mDirect != null && highlight == null) { + c.drawText(mDirect, 0, mBottom - mDesc, mPaint); + } else { + super.draw(c, highlight, highlightpaint, cursorOffset); + } + } + + /** + * Callback for the ellipsizer to report what region it ellipsized. + */ + public void ellipsized(int start, int end) { + mEllipsizedStart = start; + mEllipsizedCount = end - start; + } + + private static final char FIRST_RIGHT_TO_LEFT = '\u0590'; + + private String mDirect; + private Paint mPaint; + + /* package */ int mBottom, mDesc; // for Direct + private int mTopPadding, mBottomPadding; + private float mMax; + private int mEllipsizedWidth, mEllipsizedStart, mEllipsizedCount; + + private static final TextPaint sTemp = + new TextPaint(); + + public static class Metrics extends Paint.FontMetricsInt { + public int width; + + @Override public String toString() { + return super.toString() + " width=" + width; + } + } +} diff --git a/core/java/android/text/ClipboardManager.java b/core/java/android/text/ClipboardManager.java new file mode 100644 index 0000000..52039af --- /dev/null +++ b/core/java/android/text/ClipboardManager.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2007 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; + +import android.content.Context; +import android.os.RemoteException; +import android.os.Handler; +import android.os.IBinder; +import android.os.ServiceManager; +import android.util.Log; + +/** + * Interface to the clipboard service, for placing and retrieving text in + * the global clipboard. + * + * <p> + * You do not instantiate this class directly; instead, retrieve it through + * {@link android.content.Context#getSystemService}. + * + * @see android.content.Context#getSystemService + */ +public class ClipboardManager { + private static IClipboard sService; + + private Context mContext; + + static private IClipboard getService() { + if (sService != null) { + return sService; + } + IBinder b = ServiceManager.getService("clipboard"); + sService = IClipboard.Stub.asInterface(b); + return sService; + } + + /** {@hide} */ + public ClipboardManager(Context context, Handler handler) { + mContext = context; + } + + /** + * Returns the text on the clipboard. It will eventually be possible + * to store types other than text too, in which case this will return + * null if the type cannot be coerced to text. + */ + public CharSequence getText() { + try { + return getService().getClipboardText(); + } catch (RemoteException e) { + return null; + } + } + + /** + * Sets the contents of the clipboard to the specified text. + */ + public void setText(CharSequence text) { + try { + getService().setClipboardText(text); + } catch (RemoteException e) { + } + } + + /** + * Returns true if the clipboard contains text; false otherwise. + */ + public boolean hasText() { + try { + return getService().hasClipboardText(); + } catch (RemoteException e) { + return false; + } + } +} diff --git a/core/java/android/text/DynamicLayout.java b/core/java/android/text/DynamicLayout.java new file mode 100644 index 0000000..14e5655 --- /dev/null +++ b/core/java/android/text/DynamicLayout.java @@ -0,0 +1,503 @@ +/* + * Copyright (C) 2006 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; + +import android.graphics.Paint; +import android.text.style.UpdateLayout; +import android.text.style.WrapTogetherSpan; + +import java.lang.ref.WeakReference; + +/** + * DynamicLayout is a text layout that updates itself as the text is edited. + * <p>This is used by widgets to control text layout. You should not need + * to use this class directly unless you are implementing your own widget + * or custom display object, or need to call + * {@link android.graphics.Canvas#drawText(java.lang.CharSequence, int, int, float, float, android.graphics.Paint) + * Canvas.drawText()} directly.</p> + */ +public class DynamicLayout +extends Layout +{ + private static final int PRIORITY = 128; + + /** + * Make a layout for the specified text that will be updated as + * the text is changed. + */ + public DynamicLayout(CharSequence base, + TextPaint paint, + int width, Alignment align, + float spacingmult, float spacingadd, + boolean includepad) { + this(base, base, paint, width, align, spacingmult, spacingadd, + includepad); + } + + /** + * Make a layout for the transformed text (password transformation + * being the primary example of a transformation) + * that will be updated as the base text is changed. + */ + public DynamicLayout(CharSequence base, CharSequence display, + TextPaint paint, + int width, Alignment align, + float spacingmult, float spacingadd, + boolean includepad) { + this(base, display, paint, width, align, spacingmult, spacingadd, + includepad, null, 0); + } + + /** + * Make a layout for the transformed text (password transformation + * being the primary example of a transformation) + * that will be updated as the base text is changed. + * If ellipsize is non-null, the Layout will ellipsize the text + * down to ellipsizedWidth. + */ + public DynamicLayout(CharSequence base, CharSequence display, + TextPaint paint, + int width, Alignment align, + float spacingmult, float spacingadd, + boolean includepad, + TextUtils.TruncateAt ellipsize, int ellipsizedWidth) { + super((ellipsize == null) + ? display + : (display instanceof Spanned) + ? new SpannedEllipsizer(display) + : new Ellipsizer(display), + paint, width, align, spacingmult, spacingadd); + + mBase = base; + mDisplay = display; + + if (ellipsize != null) { + mInts = new PackedIntVector(COLUMNS_ELLIPSIZE); + mEllipsizedWidth = ellipsizedWidth; + mEllipsizeAt = ellipsize; + } else { + mInts = new PackedIntVector(COLUMNS_NORMAL); + mEllipsizedWidth = width; + mEllipsizeAt = ellipsize; + } + + mObjects = new PackedObjectVector<Directions>(1); + + mIncludePad = includepad; + + /* + * This is annoying, but we can't refer to the layout until + * superclass construction is finished, and the superclass + * constructor wants the reference to the display text. + * + * This will break if the superclass constructor ever actually + * cares about the content instead of just holding the reference. + */ + if (ellipsize != null) { + Ellipsizer e = (Ellipsizer) getText(); + + e.mLayout = this; + e.mWidth = ellipsizedWidth; + e.mMethod = ellipsize; + mEllipsize = true; + } + + // Initial state is a single line with 0 characters (0 to 0), + // with top at 0 and bottom at whatever is natural, and + // undefined ellipsis. + + int[] start; + + if (ellipsize != null) { + start = new int[COLUMNS_ELLIPSIZE]; + start[ELLIPSIS_START] = ELLIPSIS_UNDEFINED; + } else { + start = new int[COLUMNS_NORMAL]; + } + + Directions[] dirs = new Directions[] { DIRS_ALL_LEFT_TO_RIGHT }; + + Paint.FontMetricsInt fm = paint.getFontMetricsInt(); + int asc = fm.ascent; + int desc = fm.descent; + + start[DIR] = DIR_LEFT_TO_RIGHT << DIR_SHIFT; + start[TOP] = 0; + start[DESCENT] = desc; + mInts.insertAt(0, start); + + start[TOP] = desc - asc; + mInts.insertAt(1, start); + + mObjects.insertAt(0, dirs); + + // Update from 0 characters to whatever the real text is + + reflow(base, 0, 0, base.length()); + + if (base instanceof Spannable) { + if (mWatcher == null) + mWatcher = new ChangeWatcher(this); + + // Strip out any watchers for other DynamicLayouts. + Spannable sp = (Spannable) base; + ChangeWatcher[] spans = sp.getSpans(0, sp.length(), ChangeWatcher.class); + for (int i = 0; i < spans.length; i++) + sp.removeSpan(spans[i]); + + sp.setSpan(mWatcher, 0, base.length(), + Spannable.SPAN_INCLUSIVE_INCLUSIVE | + (PRIORITY << Spannable.SPAN_PRIORITY_SHIFT)); + } + } + + private void reflow(CharSequence s, int where, int before, int after) { + if (s != mBase) + return; + + CharSequence text = mDisplay; + int len = text.length(); + + // seek back to the start of the paragraph + + int find = TextUtils.lastIndexOf(text, '\n', where - 1); + if (find < 0) + find = 0; + else + find = find + 1; + + { + int diff = where - find; + before += diff; + after += diff; + where -= diff; + } + + // seek forward to the end of the paragraph + + int look = TextUtils.indexOf(text, '\n', where + after); + if (look < 0) + look = len; + else + look++; // we want the index after the \n + + int change = look - (where + after); + before += change; + after += change; + + // seek further out to cover anything that is forced to wrap together + + if (text instanceof Spanned) { + Spanned sp = (Spanned) text; + boolean again; + + do { + again = false; + + Object[] force = sp.getSpans(where, where + after, + WrapTogetherSpan.class); + + for (int i = 0; i < force.length; i++) { + int st = sp.getSpanStart(force[i]); + int en = sp.getSpanEnd(force[i]); + + if (st < where) { + again = true; + + int diff = where - st; + before += diff; + after += diff; + where -= diff; + } + + if (en > where + after) { + again = true; + + int diff = en - (where + after); + before += diff; + after += diff; + } + } + } while (again); + } + + // find affected region of old layout + + int startline = getLineForOffset(where); + int startv = getLineTop(startline); + + int endline = getLineForOffset(where + before); + if (where + after == len) + endline = getLineCount(); + int endv = getLineTop(endline); + boolean islast = (endline == getLineCount()); + + // generate new layout for affected text + + StaticLayout reflowed; + + synchronized (sLock) { + reflowed = sStaticLayout; + sStaticLayout = null; + } + + if (reflowed == null) + reflowed = new StaticLayout(true); + + reflowed.generate(text, where, where + after, + getPaint(), getWidth(), getAlignment(), + getSpacingMultiplier(), getSpacingAdd(), + false, true, mEllipsize, + mEllipsizedWidth, mEllipsizeAt); + int n = reflowed.getLineCount(); + + // If the new layout has a blank line at the end, but it is not + // the very end of the buffer, then we already have a line that + // starts there, so disregard the blank line. + + if (where + after != len && + reflowed.getLineStart(n - 1) == where + after) + n--; + + // remove affected lines from old layout + + mInts.deleteAt(startline, endline - startline); + mObjects.deleteAt(startline, endline - startline); + + // adjust offsets in layout for new height and offsets + + int ht = reflowed.getLineTop(n); + int toppad = 0, botpad = 0; + + if (mIncludePad && startline == 0) { + toppad = reflowed.getTopPadding(); + mTopPadding = toppad; + ht -= toppad; + } + if (mIncludePad && islast) { + botpad = reflowed.getBottomPadding(); + mBottomPadding = botpad; + ht += botpad; + } + + mInts.adjustValuesBelow(startline, START, after - before); + mInts.adjustValuesBelow(startline, TOP, startv - endv + ht); + + // insert new layout + + int[] ints; + + if (mEllipsize) { + ints = new int[COLUMNS_ELLIPSIZE]; + ints[ELLIPSIS_START] = ELLIPSIS_UNDEFINED; + } else { + ints = new int[COLUMNS_NORMAL]; + } + + Directions[] objects = new Directions[1]; + + + for (int i = 0; i < n; i++) { + ints[START] = reflowed.getLineStart(i) | + (reflowed.getParagraphDirection(i) << DIR_SHIFT) | + (reflowed.getLineContainsTab(i) ? TAB_MASK : 0); + + int top = reflowed.getLineTop(i) + startv; + if (i > 0) + top -= toppad; + ints[TOP] = top; + + int desc = reflowed.getLineDescent(i); + if (i == n - 1) + desc += botpad; + + ints[DESCENT] = desc; + objects[0] = reflowed.getLineDirections(i); + + if (mEllipsize) { + ints[ELLIPSIS_START] = reflowed.getEllipsisStart(i); + ints[ELLIPSIS_COUNT] = reflowed.getEllipsisCount(i); + } + + mInts.insertAt(startline + i, ints); + mObjects.insertAt(startline + i, objects); + } + + synchronized (sLock) { + sStaticLayout = reflowed; + } + } + + private void dump(boolean show) { + int n = getLineCount(); + + for (int i = 0; i < n; i++) { + System.out.print("line " + i + ": " + getLineStart(i) + " to " + getLineEnd(i) + " "); + + if (show) { + System.out.print(getText().subSequence(getLineStart(i), + getLineEnd(i))); + } + + System.out.println(""); + } + + System.out.println(""); + } + + public int getLineCount() { + return mInts.size() - 1; + } + + public int getLineTop(int line) { + return mInts.getValue(line, TOP); + } + + public int getLineDescent(int line) { + return mInts.getValue(line, DESCENT); + } + + public int getLineStart(int line) { + return mInts.getValue(line, START) & START_MASK; + } + + public boolean getLineContainsTab(int line) { + return (mInts.getValue(line, TAB) & TAB_MASK) != 0; + } + + public int getParagraphDirection(int line) { + return mInts.getValue(line, DIR) >> DIR_SHIFT; + } + + public final Directions getLineDirections(int line) { + return mObjects.getValue(line, 0); + } + + public int getTopPadding() { + return mTopPadding; + } + + public int getBottomPadding() { + return mBottomPadding; + } + + @Override + public int getEllipsizedWidth() { + return mEllipsizedWidth; + } + + private static class ChangeWatcher + implements TextWatcher, SpanWatcher + { + public ChangeWatcher(DynamicLayout layout) { + mLayout = new WeakReference(layout); + } + + private void reflow(CharSequence s, int where, int before, int after) { + DynamicLayout ml = (DynamicLayout) mLayout.get(); + + if (ml != null) + ml.reflow(s, where, before, after); + else if (s instanceof Spannable) + ((Spannable) s).removeSpan(this); + } + + public void beforeTextChanged(CharSequence s, + int where, int before, int after) { + ; + } + + public void onTextChanged(CharSequence s, + int where, int before, int after) { + reflow(s, where, before, after); + } + + public void afterTextChanged(Editable s) { + ; + } + + public void onSpanAdded(Spannable s, Object o, int start, int end) { + if (o instanceof UpdateLayout) + reflow(s, start, end - start, end - start); + } + + public void onSpanRemoved(Spannable s, Object o, int start, int end) { + if (o instanceof UpdateLayout) + reflow(s, start, end - start, end - start); + } + + public void onSpanChanged(Spannable s, Object o, int start, int end, + int nstart, int nend) { + if (o instanceof UpdateLayout) { + reflow(s, start, end - start, end - start); + reflow(s, nstart, nend - nstart, nend - nstart); + } + } + + private WeakReference mLayout; + } + + public int getEllipsisStart(int line) { + if (mEllipsizeAt == null) { + return 0; + } + + return mInts.getValue(line, ELLIPSIS_START); + } + + public int getEllipsisCount(int line) { + if (mEllipsizeAt == null) { + return 0; + } + + return mInts.getValue(line, ELLIPSIS_COUNT); + } + + private CharSequence mBase; + private CharSequence mDisplay; + private ChangeWatcher mWatcher; + private boolean mIncludePad; + private boolean mEllipsize; + private int mEllipsizedWidth; + private TextUtils.TruncateAt mEllipsizeAt; + + private PackedIntVector mInts; + private PackedObjectVector<Directions> mObjects; + + private int mTopPadding, mBottomPadding; + + private static StaticLayout sStaticLayout = new StaticLayout(true); + private static Object sLock = new Object(); + + private static final int START = 0; + private static final int DIR = START; + private static final int TAB = START; + private static final int TOP = 1; + private static final int DESCENT = 2; + private static final int COLUMNS_NORMAL = 3; + + private static final int ELLIPSIS_START = 3; + private static final int ELLIPSIS_COUNT = 4; + private static final int COLUMNS_ELLIPSIZE = 5; + + private static final int START_MASK = 0x1FFFFFFF; + private static final int DIR_MASK = 0xC0000000; + private static final int DIR_SHIFT = 30; + private static final int TAB_MASK = 0x20000000; + + private static final int ELLIPSIS_UNDEFINED = 0x80000000; +} diff --git a/core/java/android/text/Editable.java b/core/java/android/text/Editable.java new file mode 100644 index 0000000..a284a00 --- /dev/null +++ b/core/java/android/text/Editable.java @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2006 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; + +/** + * This is the interface for text whose content and markup + * can be changed (as opposed + * to immutable text like Strings). If you make a {@link DynamicLayout} + * of an Editable, the layout will be reflowed as the text is changed. + */ +public interface Editable +extends CharSequence, GetChars, Spannable, Appendable +{ + /** + * Replaces the specified range (<code>st…en</code>) of text in this + * Editable with a copy of the slice <code>start…end</code> from + * <code>source</code>. The destination slice may be empty, in which case + * the operation is an insertion, or the source slice may be empty, + * in which case the operation is a deletion. + * <p> + * Before the change is committed, each filter that was set with + * {@link #setFilters} is given the opportunity to modify the + * <code>source</code> text. + * <p> + * If <code>source</code> + * is Spanned, the spans from it are preserved into the Editable. + * Existing spans within the Editable that entirely cover the replaced + * range are retained, but any that were strictly within the range + * that was replaced are removed. As a special case, the cursor + * position is preserved even when the entire range where it is + * located is replaced. + * @return a reference to this object. + */ + public Editable replace(int st, int en, CharSequence source, int start, int end); + + /** + * Convenience for replace(st, en, text, 0, text.length()) + * @see #replace(int, int, CharSequence, int, int) + */ + public Editable replace(int st, int en, CharSequence text); + + /** + * Convenience for replace(where, where, text, start, end) + * @see #replace(int, int, CharSequence, int, int) + */ + public Editable insert(int where, CharSequence text, int start, int end); + + /** + * Convenience for replace(where, where, text, 0, text.length()); + * @see #replace(int, int, CharSequence, int, int) + */ + public Editable insert(int where, CharSequence text); + + /** + * Convenience for replace(st, en, "", 0, 0) + * @see #replace(int, int, CharSequence, int, int) + */ + public Editable delete(int st, int en); + + /** + * Convenience for replace(length(), length(), text, 0, text.length()) + * @see #replace(int, int, CharSequence, int, int) + */ + public Editable append(CharSequence text); + + /** + * Convenience for replace(length(), length(), text, start, end) + * @see #replace(int, int, CharSequence, int, int) + */ + public Editable append(CharSequence text, int start, int end); + + /** + * Convenience for append(String.valueOf(text)). + * @see #replace(int, int, CharSequence, int, int) + */ + public Editable append(char text); + + /** + * Convenience for replace(0, length(), "", 0, 0) + * @see #replace(int, int, CharSequence, int, int) + * Note that this clears the text, not the spans; + * use {@link #clearSpans} if you need that. + */ + public void clear(); + + /** + * Removes all spans from the Editable, as if by calling + * {@link #removeSpan} on each of them. + */ + public void clearSpans(); + + /** + * Sets the series of filters that will be called in succession + * whenever the text of this Editable is changed, each of which has + * the opportunity to limit or transform the text that is being inserted. + */ + public void setFilters(InputFilter[] filters); + + /** + * Returns the array of input filters that are currently applied + * to changes to this Editable. + */ + public InputFilter[] getFilters(); + + /** + * Factory used by TextView to create new Editables. You can subclass + * it to provide something other than SpannableStringBuilder. + */ + public static class Factory { + private static Editable.Factory sInstance = new Editable.Factory(); + + /** + * Returns the standard Editable Factory. + */ + public static Editable.Factory getInstance() { + return sInstance; + } + + /** + * Returns a new SpannedStringBuilder from the specified + * CharSequence. You can override this to provide + * a different kind of Spanned. + */ + public Editable newEditable(CharSequence source) { + return new SpannableStringBuilder(source); + } + } +} diff --git a/core/java/android/text/GetChars.java b/core/java/android/text/GetChars.java new file mode 100644 index 0000000..348a911 --- /dev/null +++ b/core/java/android/text/GetChars.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2006 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; + +/** + * Please implement this interface if your CharSequence has a + * getChars() method like the one in String that is faster than + * calling charAt() multiple times. + */ +public interface GetChars +extends CharSequence +{ + /** + * Exactly like String.getChars(): copy chars <code>start</code> + * through <code>end - 1</code> from this CharSequence into <code>dest</code> + * beginning at offset <code>destoff</code>. + */ + public void getChars(int start, int end, char[] dest, int destoff); +} diff --git a/core/java/android/text/GraphicsOperations.java b/core/java/android/text/GraphicsOperations.java new file mode 100644 index 0000000..c3bd0ae --- /dev/null +++ b/core/java/android/text/GraphicsOperations.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2008 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; + +import android.graphics.Canvas; +import android.graphics.Paint; + +/** + * Please implement this interface if your CharSequence can do quick + * draw/measure/widths calculations from an internal array. + * {@hide} + */ +public interface GraphicsOperations +extends CharSequence +{ + /** + * Just like {@link Canvas#drawText}. + */ + void drawText(Canvas c, int start, int end, + float x, float y, Paint p); + + /** + * Just like {@link Paint#measureText}. + */ + float measureText(int start, int end, Paint p); + + + /** + * Just like {@link Paint#getTextWidths}. + */ + public int getTextWidths(int start, int end, float[] widths, Paint p); +} diff --git a/core/java/android/text/Html.java b/core/java/android/text/Html.java new file mode 100644 index 0000000..90f5e4c --- /dev/null +++ b/core/java/android/text/Html.java @@ -0,0 +1,750 @@ +/* + * Copyright (C) 2007 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; + +import org.ccil.cowan.tagsoup.HTMLSchema; +import org.ccil.cowan.tagsoup.Parser; +import org.xml.sax.Attributes; +import org.xml.sax.ContentHandler; +import org.xml.sax.InputSource; +import org.xml.sax.Locator; +import org.xml.sax.SAXException; +import org.xml.sax.XMLReader; + +import android.content.res.Resources; +import android.graphics.Typeface; +import android.graphics.drawable.Drawable; +import android.text.style.CharacterStyle; +import android.text.style.ForegroundColorSpan; +import android.text.style.ImageSpan; +import android.text.style.ParagraphStyle; +import android.text.style.QuoteSpan; +import android.text.style.RelativeSizeSpan; +import android.text.style.StrikethroughSpan; +import android.text.style.StyleSpan; +import android.text.style.SubscriptSpan; +import android.text.style.SuperscriptSpan; +import android.text.style.TypefaceSpan; +import android.text.style.URLSpan; +import android.text.style.UnderlineSpan; +import com.android.internal.util.XmlUtils; + +import java.io.IOException; +import java.io.StringReader; +import java.nio.CharBuffer; + +/** + * This class processes HTML strings into displayable styled text. + * Not all HTML tags are supported. + */ +public class Html { + /** + * Retrieves images for HTML <img> tags. + */ + public static interface ImageGetter { + /** + * This methos is called when the HTML parser encounters an + * <img> tag. The <code>source</code> argument is the + * string from the "src" attribute; the return value should be + * a Drawable representation of the image or <code>null</code> + * for a generic replacement image. Make sure you call + * setBounds() on your Drawable if it doesn't already have + * its bounds set. + */ + public Drawable getDrawable(String source); + } + + /** + * Is notified when HTML tags are encountered that the parser does + * not know how to interpret. + */ + public static interface TagHandler { + /** + * This method will be called whenn the HTML parser encounters + * a tag that it does not know how to interpret. + */ + public void handleTag(boolean opening, String tag, + Editable output, XMLReader xmlReader); + } + + private Html() { } + + /** + * Returns displayable styled text from the provided HTML string. + * Any <img> tags in the HTML will display as a generic + * replacement image which your program can then go through and + * replace with real images. + * + * <p>This uses TagSoup to handle real HTML, including all of the brokenness found in the wild. + */ + public static Spanned fromHtml(String source) { + return fromHtml(source, null, null); + } + + /** + * Lazy initialization holder for HTML parser. This class will + * a) be preloaded by the zygote, or b) not loaded until absolutely + * necessary. + */ + private static class HtmlParser { + private static final HTMLSchema schema = new HTMLSchema(); + } + + /** + * Returns displayable styled text from the provided HTML string. + * Any <img> tags in the HTML will use the specified ImageGetter + * to request a representation of the image (use null if you don't + * want this) and the specified TagHandler to handle unknown tags + * (specify null if you don't want this). + * + * <p>This uses TagSoup to handle real HTML, including all of the brokenness found in the wild. + */ + public static Spanned fromHtml(String source, ImageGetter imageGetter, + TagHandler tagHandler) { + Parser parser = new Parser(); + try { + parser.setProperty(Parser.schemaProperty, HtmlParser.schema); + } catch (org.xml.sax.SAXNotRecognizedException e) { + // Should not happen. + throw new RuntimeException(e); + } catch (org.xml.sax.SAXNotSupportedException e) { + // Should not happen. + throw new RuntimeException(e); + } + + HtmlToSpannedConverter converter = + new HtmlToSpannedConverter(source, imageGetter, tagHandler, + parser); + return converter.convert(); + } + + /** + * Returns an HTML representation of the provided Spanned text. + */ + public static String toHtml(Spanned text) { + StringBuilder out = new StringBuilder(); + int len = text.length(); + + int next; + for (int i = 0; i < text.length(); i = next) { + next = text.nextSpanTransition(i, len, QuoteSpan.class); + QuoteSpan[] quotes = text.getSpans(i, next, QuoteSpan.class); + + for (QuoteSpan quote: quotes) { + out.append("<blockquote>"); + } + + withinBlockquote(out, text, i, next); + + for (QuoteSpan quote: quotes) { + out.append("</blockquote>\n"); + } + } + + return out.toString(); + } + + private static void withinBlockquote(StringBuilder out, Spanned text, + int start, int end) { + out.append("<p>"); + + int next; + for (int i = start; i < end; i = next) { + next = TextUtils.indexOf(text, '\n', i, end); + if (next < 0) { + next = end; + } + + int nl = 0; + + while (next < end && text.charAt(next) == '\n') { + nl++; + next++; + } + + withinParagraph(out, text, i, next - nl, nl, next == end); + } + + out.append("</p>\n"); + } + + private static void withinParagraph(StringBuilder out, Spanned text, + int start, int end, int nl, + boolean last) { + int next; + for (int i = start; i < end; i = next) { + next = text.nextSpanTransition(i, end, CharacterStyle.class); + CharacterStyle[] style = text.getSpans(i, next, + CharacterStyle.class); + + for (int j = 0; j < style.length; j++) { + if (style[j] instanceof StyleSpan) { + int s = ((StyleSpan) style[j]).getStyle(); + + if ((s & Typeface.BOLD) != 0) { + out.append("<b>"); + } + if ((s & Typeface.ITALIC) != 0) { + out.append("<i>"); + } + } + if (style[j] instanceof TypefaceSpan) { + String s = ((TypefaceSpan) style[j]).getFamily(); + + if (s.equals("monospace")) { + out.append("<tt>"); + } + } + if (style[j] instanceof SuperscriptSpan) { + out.append("<sup>"); + } + if (style[j] instanceof SubscriptSpan) { + out.append("<sub>"); + } + if (style[j] instanceof UnderlineSpan) { + out.append("<u>"); + } + if (style[j] instanceof StrikethroughSpan) { + out.append("<strike>"); + } + if (style[j] instanceof URLSpan) { + out.append("<a href=\""); + out.append(((URLSpan) style[j]).getURL()); + out.append("\">"); + } + if (style[j] instanceof ImageSpan) { + out.append("<img src=\""); + out.append(((ImageSpan) style[j]).getSource()); + out.append("\">"); + + // Don't output the dummy character underlying the image. + i = next; + } + } + + withinStyle(out, text, i, next); + + for (int j = style.length - 1; j >= 0; j--) { + if (style[j] instanceof URLSpan) { + out.append("</a>"); + } + if (style[j] instanceof StrikethroughSpan) { + out.append("</strike>"); + } + if (style[j] instanceof UnderlineSpan) { + out.append("</u>"); + } + if (style[j] instanceof SubscriptSpan) { + out.append("</sub>"); + } + if (style[j] instanceof SuperscriptSpan) { + out.append("</sup>"); + } + if (style[j] instanceof TypefaceSpan) { + String s = ((TypefaceSpan) style[j]).getFamily(); + + if (s.equals("monospace")) { + out.append("</tt>"); + } + } + if (style[j] instanceof StyleSpan) { + int s = ((StyleSpan) style[j]).getStyle(); + + if ((s & Typeface.BOLD) != 0) { + out.append("</b>"); + } + if ((s & Typeface.ITALIC) != 0) { + out.append("</i>"); + } + } + } + } + + String p = last ? "" : "</p>\n<p>"; + + if (nl == 1) { + out.append("<br>\n"); + } else if (nl == 2) { + out.append(p); + } else { + for (int i = 2; i < nl; i++) { + out.append("<br>"); + } + + out.append(p); + } + } + + private static void withinStyle(StringBuilder out, Spanned text, + int start, int end) { + for (int i = start; i < end; i++) { + char c = text.charAt(i); + + if (c == '<') { + out.append("<"); + } else if (c == '>') { + out.append(">"); + } else if (c == '&') { + out.append("&"); + } else if (c > 0x7E || c < ' ') { + out.append("&#" + ((int) c) + ";"); + } else if (c == ' ') { + while (i + 1 < end && text.charAt(i + 1) == ' ') { + out.append(" "); + i++; + } + + out.append(' '); + } else { + out.append(c); + } + } + } +} + +class HtmlToSpannedConverter implements ContentHandler { + + private static final float[] HEADER_SIZES = { + 1.5f, 1.4f, 1.3f, 1.2f, 1.1f, 1f, + }; + + private String mSource; + private XMLReader mReader; + private SpannableStringBuilder mSpannableStringBuilder; + private Html.ImageGetter mImageGetter; + private Html.TagHandler mTagHandler; + + public HtmlToSpannedConverter( + String source, Html.ImageGetter imageGetter, Html.TagHandler tagHandler, + Parser parser) { + mSource = source; + mSpannableStringBuilder = new SpannableStringBuilder(); + mImageGetter = imageGetter; + mTagHandler = tagHandler; + mReader = parser; + } + + public Spanned convert() { + + mReader.setContentHandler(this); + try { + mReader.parse(new InputSource(new StringReader(mSource))); + } catch (IOException e) { + // We are reading from a string. There should not be IO problems. + throw new RuntimeException(e); + } catch (SAXException e) { + // TagSoup doesn't throw parse exceptions. + throw new RuntimeException(e); + } + + // Fix flags and range for paragraph-type markup. + Object[] obj = mSpannableStringBuilder.getSpans(0, mSpannableStringBuilder.length(), ParagraphStyle.class); + for (int i = 0; i < obj.length; i++) { + int start = mSpannableStringBuilder.getSpanStart(obj[i]); + int end = mSpannableStringBuilder.getSpanEnd(obj[i]); + + // If the last line of the range is blank, back off by one. + if (end - 2 >= 0) { + if (mSpannableStringBuilder.charAt(end - 1) == '\n' && + mSpannableStringBuilder.charAt(end - 2) == '\n') { + end--; + } + } + + if (end == start) { + mSpannableStringBuilder.removeSpan(obj[i]); + } else { + mSpannableStringBuilder.setSpan(obj[i], start, end, Spannable.SPAN_PARAGRAPH); + } + } + + return mSpannableStringBuilder; + } + + private void handleStartTag(String tag, Attributes attributes) { + if (tag.equalsIgnoreCase("br")) { + // We don't need to handle this. TagSoup will ensure that there's a </br> for each <br> + // so we can safely emite the linebreaks when we handle the close tag. + } else if (tag.equalsIgnoreCase("p")) { + handleP(mSpannableStringBuilder); + } else if (tag.equalsIgnoreCase("div")) { + handleP(mSpannableStringBuilder); + } else if (tag.equalsIgnoreCase("em")) { + start(mSpannableStringBuilder, new Bold()); + } else if (tag.equalsIgnoreCase("b")) { + start(mSpannableStringBuilder, new Bold()); + } else if (tag.equalsIgnoreCase("strong")) { + start(mSpannableStringBuilder, new Italic()); + } else if (tag.equalsIgnoreCase("cite")) { + start(mSpannableStringBuilder, new Italic()); + } else if (tag.equalsIgnoreCase("dfn")) { + start(mSpannableStringBuilder, new Italic()); + } else if (tag.equalsIgnoreCase("i")) { + start(mSpannableStringBuilder, new Italic()); + } else if (tag.equalsIgnoreCase("big")) { + start(mSpannableStringBuilder, new Big()); + } else if (tag.equalsIgnoreCase("small")) { + start(mSpannableStringBuilder, new Small()); + } else if (tag.equalsIgnoreCase("font")) { + startFont(mSpannableStringBuilder, attributes); + } else if (tag.equalsIgnoreCase("blockquote")) { + handleP(mSpannableStringBuilder); + start(mSpannableStringBuilder, new Blockquote()); + } else if (tag.equalsIgnoreCase("tt")) { + start(mSpannableStringBuilder, new Monospace()); + } else if (tag.equalsIgnoreCase("a")) { + startA(mSpannableStringBuilder, attributes); + } else if (tag.equalsIgnoreCase("u")) { + start(mSpannableStringBuilder, new Underline()); + } else if (tag.equalsIgnoreCase("sup")) { + start(mSpannableStringBuilder, new Super()); + } else if (tag.equalsIgnoreCase("sub")) { + start(mSpannableStringBuilder, new Sub()); + } else if (tag.length() == 2 && + Character.toLowerCase(tag.charAt(0)) == 'h' && + tag.charAt(1) >= '1' && tag.charAt(1) <= '6') { + handleP(mSpannableStringBuilder); + start(mSpannableStringBuilder, new Header(tag.charAt(1) - '1')); + } else if (tag.equalsIgnoreCase("img")) { + startImg(mSpannableStringBuilder, attributes, mImageGetter); + } else if (mTagHandler != null) { + mTagHandler.handleTag(true, tag, mSpannableStringBuilder, mReader); + } + } + + private void handleEndTag(String tag) { + if (tag.equalsIgnoreCase("br")) { + handleBr(mSpannableStringBuilder); + } else if (tag.equalsIgnoreCase("p")) { + handleP(mSpannableStringBuilder); + } else if (tag.equalsIgnoreCase("div")) { + handleP(mSpannableStringBuilder); + } else if (tag.equalsIgnoreCase("em")) { + end(mSpannableStringBuilder, Bold.class, new StyleSpan(Typeface.BOLD)); + } else if (tag.equalsIgnoreCase("b")) { + end(mSpannableStringBuilder, Bold.class, new StyleSpan(Typeface.BOLD)); + } else if (tag.equalsIgnoreCase("strong")) { + end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC)); + } else if (tag.equalsIgnoreCase("cite")) { + end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC)); + } else if (tag.equalsIgnoreCase("dfn")) { + end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC)); + } else if (tag.equalsIgnoreCase("i")) { + end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC)); + } else if (tag.equalsIgnoreCase("big")) { + end(mSpannableStringBuilder, Big.class, new RelativeSizeSpan(1.25f)); + } else if (tag.equalsIgnoreCase("small")) { + end(mSpannableStringBuilder, Small.class, new RelativeSizeSpan(0.8f)); + } else if (tag.equalsIgnoreCase("font")) { + endFont(mSpannableStringBuilder); + } else if (tag.equalsIgnoreCase("blockquote")) { + handleP(mSpannableStringBuilder); + end(mSpannableStringBuilder, Blockquote.class, new QuoteSpan()); + } else if (tag.equalsIgnoreCase("tt")) { + end(mSpannableStringBuilder, Monospace.class, + new TypefaceSpan("monospace")); + } else if (tag.equalsIgnoreCase("a")) { + endA(mSpannableStringBuilder); + } else if (tag.equalsIgnoreCase("u")) { + end(mSpannableStringBuilder, Underline.class, new UnderlineSpan()); + } else if (tag.equalsIgnoreCase("sup")) { + end(mSpannableStringBuilder, Super.class, new SuperscriptSpan()); + } else if (tag.equalsIgnoreCase("sub")) { + end(mSpannableStringBuilder, Sub.class, new SubscriptSpan()); + } else if (tag.length() == 2 && + Character.toLowerCase(tag.charAt(0)) == 'h' && + tag.charAt(1) >= '1' && tag.charAt(1) <= '6') { + handleP(mSpannableStringBuilder); + endHeader(mSpannableStringBuilder); + } else if (mTagHandler != null) { + mTagHandler.handleTag(false, tag, mSpannableStringBuilder, mReader); + } + } + + private static void handleP(SpannableStringBuilder text) { + int len = text.length(); + + if (len >= 1 && text.charAt(len - 1) == '\n') { + if (len >= 2 && text.charAt(len - 2) == '\n') { + return; + } + + text.append("\n"); + return; + } + + if (len != 0) { + text.append("\n\n"); + } + } + + private static void handleBr(SpannableStringBuilder text) { + text.append("\n"); + } + + private static Object getLast(Spanned text, Class kind) { + /* + * This knows that the last returned object from getSpans() + * will be the most recently added. + */ + Object[] objs = text.getSpans(0, text.length(), kind); + + if (objs.length == 0) { + return null; + } else { + return objs[objs.length - 1]; + } + } + + private static void start(SpannableStringBuilder text, Object mark) { + int len = text.length(); + text.setSpan(mark, len, len, Spannable.SPAN_MARK_MARK); + } + + private static void end(SpannableStringBuilder text, Class kind, + Object repl) { + int len = text.length(); + Object obj = getLast(text, kind); + int where = text.getSpanStart(obj); + + text.removeSpan(obj); + + if (where != len) { + text.setSpan(repl, where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + + return; + } + + private static void startImg(SpannableStringBuilder text, + Attributes attributes, Html.ImageGetter img) { + String src = attributes.getValue("", "src"); + Drawable d = null; + + if (img != null) { + d = img.getDrawable(src); + } + + if (d == null) { + d = Resources.getSystem(). + getDrawable(com.android.internal.R.drawable.unknown_image); + d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight()); + } + + int len = text.length(); + text.append("\uFFFC"); + + text.setSpan(new ImageSpan(d, src), len, text.length(), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + + private static void startFont(SpannableStringBuilder text, + Attributes attributes) { + String color = attributes.getValue("", "color"); + String face = attributes.getValue("", "face"); + + int len = text.length(); + text.setSpan(new Font(color, face), len, len, Spannable.SPAN_MARK_MARK); + } + + private static void endFont(SpannableStringBuilder text) { + int len = text.length(); + Object obj = getLast(text, Font.class); + int where = text.getSpanStart(obj); + + text.removeSpan(obj); + + if (where != len) { + Font f = (Font) obj; + + if (f.mColor != null) { + int c = -1; + + if (f.mColor.equalsIgnoreCase("aqua")) { + c = 0x00FFFF; + } else if (f.mColor.equalsIgnoreCase("black")) { + c = 0x000000; + } else if (f.mColor.equalsIgnoreCase("blue")) { + c = 0x0000FF; + } else if (f.mColor.equalsIgnoreCase("fuchsia")) { + c = 0xFF00FF; + } else if (f.mColor.equalsIgnoreCase("green")) { + c = 0x008000; + } else if (f.mColor.equalsIgnoreCase("grey")) { + c = 0x808080; + } else if (f.mColor.equalsIgnoreCase("lime")) { + c = 0x00FF00; + } else if (f.mColor.equalsIgnoreCase("maroon")) { + c = 0x800000; + } else if (f.mColor.equalsIgnoreCase("navy")) { + c = 0x000080; + } else if (f.mColor.equalsIgnoreCase("olive")) { + c = 0x808000; + } else if (f.mColor.equalsIgnoreCase("purple")) { + c = 0x800080; + } else if (f.mColor.equalsIgnoreCase("red")) { + c = 0xFF0000; + } else if (f.mColor.equalsIgnoreCase("silver")) { + c = 0xC0C0C0; + } else if (f.mColor.equalsIgnoreCase("teal")) { + c = 0x008080; + } else if (f.mColor.equalsIgnoreCase("white")) { + c = 0xFFFFFF; + } else if (f.mColor.equalsIgnoreCase("yellow")) { + c = 0xFFFF00; + } else { + try { + c = XmlUtils.convertValueToInt(f.mColor, -1); + } catch (NumberFormatException nfe) { + // Can't understand the color, so just drop it. + } + } + + if (c != -1) { + text.setSpan(new ForegroundColorSpan(c | 0xFF000000), + where, len, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + + if (f.mFace != null) { + text.setSpan(new TypefaceSpan(f.mFace), where, len, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + } + + private static void startA(SpannableStringBuilder text, Attributes attributes) { + String href = attributes.getValue("", "href"); + + int len = text.length(); + text.setSpan(new Href(href), len, len, Spannable.SPAN_MARK_MARK); + } + + private static void endA(SpannableStringBuilder text) { + int len = text.length(); + Object obj = getLast(text, Href.class); + int where = text.getSpanStart(obj); + + text.removeSpan(obj); + + if (where != len) { + Href h = (Href) obj; + + if (h.mHref != null) { + text.setSpan(new URLSpan(h.mHref), where, len, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + } + + private static void endHeader(SpannableStringBuilder text) { + int len = text.length(); + Object obj = getLast(text, Header.class); + + int where = text.getSpanStart(obj); + + text.removeSpan(obj); + + // Back off not to change only the text, not the blank line. + while (len > where && text.charAt(len - 1) == '\n') { + len--; + } + + if (where != len) { + Header h = (Header) obj; + + text.setSpan(new RelativeSizeSpan(HEADER_SIZES[h.mLevel]), + where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + text.setSpan(new StyleSpan(Typeface.BOLD), + where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + + public void setDocumentLocator(Locator locator) { + } + + public void startDocument() throws SAXException { + } + + public void endDocument() throws SAXException { + } + + public void startPrefixMapping(String prefix, String uri) throws SAXException { + } + + public void endPrefixMapping(String prefix) throws SAXException { + } + + public void startElement(String uri, String localName, String qName, Attributes attributes) + throws SAXException { + handleStartTag(localName, attributes); + } + + public void endElement(String uri, String localName, String qName) throws SAXException { + handleEndTag(localName); + } + + public void characters(char ch[], int start, int length) throws SAXException { + mSpannableStringBuilder.append(CharBuffer.wrap(ch, start, length)); + } + + public void ignorableWhitespace(char ch[], int start, int length) throws SAXException { + } + + public void processingInstruction(String target, String data) throws SAXException { + } + + public void skippedEntity(String name) throws SAXException { + } + + private static class Bold { } + private static class Italic { } + private static class Underline { } + private static class Big { } + private static class Small { } + private static class Monospace { } + private static class Blockquote { } + private static class Super { } + private static class Sub { } + + private static class Font { + public String mColor; + public String mFace; + + public Font(String color, String face) { + mColor = color; + mFace = face; + } + } + + private static class Href { + public String mHref; + + public Href(String href) { + mHref = href; + } + } + + private static class Header { + private int mLevel; + + public Header(int level) { + mLevel = level; + } + } +} diff --git a/core/java/android/text/IClipboard.aidl b/core/java/android/text/IClipboard.aidl new file mode 100644 index 0000000..4deb5c8 --- /dev/null +++ b/core/java/android/text/IClipboard.aidl @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2008, 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; + +/** + * Programming interface to the clipboard, which allows copying and pasting + * between applications. + * {@hide} + */ +interface IClipboard { + /** + * Returns the text on the clipboard. It will eventually be possible + * to store types other than text too, in which case this will return + * null if the type cannot be coerced to text. + */ + CharSequence getClipboardText(); + + /** + * Sets the contents of the clipboard to the specified text. + */ + void setClipboardText(CharSequence text); + + /** + * Returns true if the clipboard contains text; false otherwise. + */ + boolean hasClipboardText(); +} + diff --git a/core/java/android/text/InputFilter.java b/core/java/android/text/InputFilter.java new file mode 100644 index 0000000..e1563ae --- /dev/null +++ b/core/java/android/text/InputFilter.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2006 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; + +/** + * InputFilters can be attached to {@link Editable}s to constrain the + * changes that can be made to them. + */ +public interface InputFilter +{ + /** + * This method is called when the buffer is going to replace the + * range <code>dstart … dend</code> of <code>dest</code> + * with the new text from the range <code>start … end</code> + * of <code>source</code>. Return the CharSequence that you would + * like to have placed there instead, including an empty string + * if appropriate, or <code>null</code> to accept the original + * replacement. Be careful to not to reject 0-length replacements, + * as this is what happens when you delete text. Also beware that + * you should not attempt to make any changes to <code>dest</code> + * from this method; you may only examine it for context. + */ + public CharSequence filter(CharSequence source, int start, int end, + Spanned dest, int dstart, int dend); + + /** + * This filter will capitalize all the lower case letters that are added + * through edits. + */ + public static class AllCaps implements InputFilter { + public CharSequence filter(CharSequence source, int start, int end, + Spanned dest, int dstart, int dend) { + for (int i = start; i < end; i++) { + if (Character.isLowerCase(source.charAt(i))) { + char[] v = new char[end - start]; + TextUtils.getChars(source, start, end, v, 0); + String s = new String(v).toUpperCase(); + + if (source instanceof Spanned) { + SpannableString sp = new SpannableString(s); + TextUtils.copySpansFrom((Spanned) source, + start, end, null, sp, 0); + return sp; + } else { + return s; + } + } + } + + return null; // keep original + } + } + + /** + * This filter will constrain edits not to make the length of the text + * greater than the specified length. + */ + public static class LengthFilter implements InputFilter { + public LengthFilter(int max) { + mMax = max; + } + + public CharSequence filter(CharSequence source, int start, int end, + Spanned dest, int dstart, int dend) { + int keep = mMax - (dest.length() - (dend - dstart)); + + if (keep <= 0) { + return ""; + } else if (keep >= end - start) { + return null; // keep original + } else { + return source.subSequence(start, start + keep); + } + } + + private int mMax; + } +} diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java new file mode 100644 index 0000000..346db49 --- /dev/null +++ b/core/java/android/text/Layout.java @@ -0,0 +1,1745 @@ +/* + * Copyright (C) 2006 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; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.Path; +import com.android.internal.util.ArrayUtils; +import android.util.Config; + +import junit.framework.Assert; +import android.text.style.*; +import android.text.method.TextKeyListener; +import android.view.KeyEvent; + +/** + * A base class that manages text layout in visual elements on + * the screen. + * <p>For text that will be edited, use a {@link DynamicLayout}, + * which will be updated as the text changes. + * For text that will not change, use a {@link StaticLayout}. + */ +public abstract class Layout { + /** + * Return how wide a layout would be necessary to display the + * specified text with one line per paragraph. + */ + public static float getDesiredWidth(CharSequence source, + TextPaint paint) { + return getDesiredWidth(source, 0, source.length(), paint); + } + + /** + * Return how wide a layout would be necessary to display the + * specified text slice with one line per paragraph. + */ + public static float getDesiredWidth(CharSequence source, + int start, int end, + TextPaint paint) { + float need = 0; + TextPaint workPaint = new TextPaint(); + + int next; + for (int i = start; i <= end; i = next) { + next = TextUtils.indexOf(source, '\n', i, end); + + if (next < 0) + next = end; + + float w = measureText(paint, workPaint, + source, i, next, null, true, null); + + if (w > need) + need = w; + + next++; + } + + return need; + } + + /** + * Subclasses of Layout use this constructor to set the display text, + * width, and other standard properties. + */ + protected Layout(CharSequence text, TextPaint paint, + int width, Alignment align, + float spacingmult, float spacingadd) { + if (width < 0) + throw new IllegalArgumentException("Layout: " + width + " < 0"); + + mText = text; + mPaint = paint; + mWorkPaint = new TextPaint(); + mWidth = width; + mAlignment = align; + mSpacingMult = spacingmult; + mSpacingAdd = spacingadd; + mSpannedText = text instanceof Spanned; + } + + /** + * Replace constructor properties of this Layout with new ones. Be careful. + */ + /* package */ void replaceWith(CharSequence text, TextPaint paint, + int width, Alignment align, + float spacingmult, float spacingadd) { + if (width < 0) { + throw new IllegalArgumentException("Layout: " + width + " < 0"); + } + + mText = text; + mPaint = paint; + mWidth = width; + mAlignment = align; + mSpacingMult = spacingmult; + mSpacingAdd = spacingadd; + mSpannedText = text instanceof Spanned; + } + + /** + * Draw this Layout on the specified Canvas. + */ + public void draw(Canvas c) { + draw(c, null, null, 0); + } + + /** + * Draw the specified rectangle from this Layout on the specified Canvas, + * with the specified path drawn between the background and the text. + */ + public void draw(Canvas c, Path highlight, Paint highlightpaint, + int cursorOffsetVertical) { + int dtop, dbottom; + + synchronized (sTempRect) { + if (!c.getClipBounds(sTempRect)) { + return; + } + + dtop = sTempRect.top; + dbottom = sTempRect.bottom; + } + + TextPaint paint = mPaint; + + int top = 0; + // getLineBottom(getLineCount() -1) just calls getLineTop(getLineCount) + int bottom = getLineTop(getLineCount()); + + + if (dtop > top) { + top = dtop; + } + if (dbottom < bottom) { + bottom = dbottom; + } + + int first = getLineForVertical(top); + int last = getLineForVertical(bottom); + + int previousLineBottom = getLineTop(first); + int previousLineEnd = getLineStart(first); + + CharSequence buf = mText; + + ParagraphStyle[] nospans = ArrayUtils.emptyArray(ParagraphStyle.class); + ParagraphStyle[] spans = nospans; + int spanend = 0; + int textLength = 0; + boolean spannedText = mSpannedText; + + if (spannedText) { + spanend = 0; + textLength = buf.length(); + for (int i = first; i <= last; i++) { + int start = previousLineEnd; + int end = getLineStart(i+1); + previousLineEnd = end; + + int ltop = previousLineBottom; + int lbottom = getLineTop(i+1); + previousLineBottom = lbottom; + int lbaseline = lbottom - getLineDescent(i); + + if (start >= spanend) { + Spanned sp = (Spanned) buf; + spanend = sp.nextSpanTransition(start, textLength, + LineBackgroundSpan.class); + spans = sp.getSpans(start, spanend, + LineBackgroundSpan.class); + } + + for (int n = 0; n < spans.length; n++) { + LineBackgroundSpan back = (LineBackgroundSpan) spans[n]; + + back.drawBackground(c, paint, 0, mWidth, + ltop, lbaseline, lbottom, + buf, start, end, + i); + } + } + // reset to their original values + spanend = 0; + previousLineBottom = getLineTop(first); + previousLineEnd = getLineStart(first); + spans = nospans; + } + + // There can be a highlight even without spans if we are drawing + // a non-spanned transformation of a spanned editing buffer. + if (highlight != null) { + if (cursorOffsetVertical != 0) { + c.translate(0, cursorOffsetVertical); + } + + c.drawPath(highlight, highlightpaint); + + if (cursorOffsetVertical != 0) { + c.translate(0, -cursorOffsetVertical); + } + } + + Alignment align = mAlignment; + + for (int i = first; i <= last; i++) { + int start = previousLineEnd; + + previousLineEnd = getLineStart(i+1); + int end = getLineVisibleEnd(i, start, previousLineEnd); + + int ltop = previousLineBottom; + int lbottom = getLineTop(i+1); + previousLineBottom = lbottom; + int lbaseline = lbottom - getLineDescent(i); + + boolean par = false; + if (spannedText) { + if (start == 0 || buf.charAt(start - 1) == '\n') { + par = true; + } + if (start >= spanend) { + + Spanned sp = (Spanned) buf; + + spanend = sp.nextSpanTransition(start, textLength, + ParagraphStyle.class); + spans = sp.getSpans(start, spanend, ParagraphStyle.class); + + align = mAlignment; + + for (int n = spans.length-1; n >= 0; n--) { + if (spans[n] instanceof AlignmentSpan) { + align = ((AlignmentSpan) spans[n]).getAlignment(); + break; + } + } + } + } + + int dir = getParagraphDirection(i); + int left = 0; + int right = mWidth; + + if (spannedText) { + final int length = spans.length; + for (int n = 0; n < length; n++) { + if (spans[n] instanceof LeadingMarginSpan) { + LeadingMarginSpan margin = (LeadingMarginSpan) spans[n]; + + if (dir == DIR_RIGHT_TO_LEFT) { + margin.drawLeadingMargin(c, paint, right, dir, ltop, + lbaseline, lbottom, buf, + start, end, par, this); + + right -= margin.getLeadingMargin(par); + } else { + margin.drawLeadingMargin(c, paint, left, dir, ltop, + lbaseline, lbottom, buf, + start, end, par, this); + + left += margin.getLeadingMargin(par); + } + } + } + } + + int x; + if (align == Alignment.ALIGN_NORMAL) { + if (dir == DIR_LEFT_TO_RIGHT) { + x = left; + } else { + x = right; + } + } else { + int max = (int)getLineMax(i, spans, false); + if (align == Alignment.ALIGN_OPPOSITE) { + if (dir == DIR_RIGHT_TO_LEFT) { + x = left + max; + } else { + x = right - max; + } + } else { + // Alignment.ALIGN_CENTER + max = max & ~1; + int half = (right - left - max) >> 1; + if (dir == DIR_RIGHT_TO_LEFT) { + x = right - half; + } else { + x = left + half; + } + } + } + + Directions directions = getLineDirections(i); + boolean hasTab = getLineContainsTab(i); + if (directions == DIRS_ALL_LEFT_TO_RIGHT && + !spannedText && !hasTab) { + if (Config.DEBUG) { + Assert.assertTrue(dir == DIR_LEFT_TO_RIGHT); + Assert.assertNotNull(c); + } + c.drawText(buf, start, end, x, lbaseline, paint); + } else { + drawText(c, buf, start, end, dir, directions, + x, ltop, lbaseline, lbottom, paint, mWorkPaint, + hasTab, spans); + } + } + } + + /** + * Return the text that is displayed by this Layout. + */ + public final CharSequence getText() { + return mText; + } + + /** + * Return the base Paint properties for this layout. + * Do NOT change the paint, which may result in funny + * drawing for this layout. + */ + public final TextPaint getPaint() { + return mPaint; + } + + /** + * Return the width of this layout. + */ + public final int getWidth() { + return mWidth; + } + + /** + * Return the width to which this Layout is ellipsizing, or + * {@link #getWidth} if it is not doing anything special. + */ + public int getEllipsizedWidth() { + return mWidth; + } + + /** + * Increase the width of this layout to the specified width. + * Be careful to use this only when you know it is appropriate -- + * it does not cause the text to reflow to use the full new width. + */ + public final void increaseWidthTo(int wid) { + if (wid < mWidth) { + throw new RuntimeException("attempted to reduce Layout width"); + } + + mWidth = wid; + } + + /** + * Return the total height of this layout. + */ + public int getHeight() { + return getLineTop(getLineCount()); // same as getLineBottom(getLineCount() - 1); + } + + /** + * Return the base alignment of this layout. + */ + public final Alignment getAlignment() { + return mAlignment; + } + + /** + * Return what the text height is multiplied by to get the line height. + */ + public final float getSpacingMultiplier() { + return mSpacingMult; + } + + /** + * Return the number of units of leading that are added to each line. + */ + public final float getSpacingAdd() { + return mSpacingAdd; + } + + /** + * Return the number of lines of text in this layout. + */ + public abstract int getLineCount(); + + /** + * Return the baseline for the specified line (0…getLineCount() - 1) + * If bounds is not null, return the top, left, right, bottom extents + * of the specified line in it. + * @param line which line to examine (0..getLineCount() - 1) + * @param bounds Optional. If not null, it returns the extent of the line + * @return the Y-coordinate of the baseline + */ + public int getLineBounds(int line, Rect bounds) { + if (bounds != null) { + bounds.left = 0; // ??? + bounds.top = getLineTop(line); + bounds.right = mWidth; // ??? + bounds.bottom = getLineBottom(line); + } + return getLineBaseline(line); + } + + /** + * Return the vertical position of the top of the specified line. + * If the specified line is one beyond the last line, returns the + * bottom of the last line. + */ + public abstract int getLineTop(int line); + + /** + * Return the descent of the specified line. + */ + public abstract int getLineDescent(int line); + + /** + * Return the text offset of the beginning of the specified line. + * If the specified line is one beyond the last line, returns the + * end of the last line. + */ + public abstract int getLineStart(int line); + + /** + * Returns the primary directionality of the paragraph containing + * the specified line. + */ + public abstract int getParagraphDirection(int line); + + /** + * Returns whether the specified line contains one or more tabs. + */ + public abstract boolean getLineContainsTab(int line); + + /** + * Returns an array of directionalities for the specified line. + * The array alternates counts of characters in left-to-right + * and right-to-left segments of the line. + */ + public abstract Directions getLineDirections(int line); + + /** + * Returns the (negative) number of extra pixels of ascent padding in the + * top line of the Layout. + */ + public abstract int getTopPadding(); + + /** + * Returns the number of extra pixels of descent padding in the + * bottom line of the Layout. + */ + public abstract int getBottomPadding(); + + /** + * Get the primary horizontal position for the specified text offset. + * This is the location where a new character would be inserted in + * the paragraph's primary direction. + */ + public float getPrimaryHorizontal(int offset) { + return getHorizontal(offset, false, true); + } + + /** + * Get the secondary horizontal position for the specified text offset. + * This is the location where a new character would be inserted in + * the direction other than the paragraph's primary direction. + */ + public float getSecondaryHorizontal(int offset) { + return getHorizontal(offset, true, true); + } + + private float getHorizontal(int offset, boolean trailing, boolean alt) { + int line = getLineForOffset(offset); + + return getHorizontal(offset, trailing, alt, line); + } + + private float getHorizontal(int offset, boolean trailing, boolean alt, + int line) { + int start = getLineStart(line); + int end = getLineVisibleEnd(line); + int dir = getParagraphDirection(line); + boolean tab = getLineContainsTab(line); + Directions directions = getLineDirections(line); + + TabStopSpan[] tabs = null; + if (tab && mText instanceof Spanned) { + tabs = ((Spanned) mText).getSpans(start, end, TabStopSpan.class); + } + + float wid = measureText(mPaint, mWorkPaint, mText, start, offset, end, + dir, directions, trailing, alt, tab, tabs); + + if (offset > end) { + if (dir == DIR_RIGHT_TO_LEFT) + wid -= measureText(mPaint, mWorkPaint, + mText, end, offset, null, tab, tabs); + else + wid += measureText(mPaint, mWorkPaint, + mText, end, offset, null, tab, tabs); + } + + Alignment align = getParagraphAlignment(line); + int left = getParagraphLeft(line); + int right = getParagraphRight(line); + + if (align == Alignment.ALIGN_NORMAL) { + if (dir == DIR_RIGHT_TO_LEFT) + return right + wid; + else + return left + wid; + } + + float max = getLineMax(line); + + if (align == Alignment.ALIGN_OPPOSITE) { + if (dir == DIR_RIGHT_TO_LEFT) + return left + max + wid; + else + return right - max + wid; + } else { /* align == Alignment.ALIGN_CENTER */ + int imax = ((int) max) & ~1; + + if (dir == DIR_RIGHT_TO_LEFT) + return right - (((right - left) - imax) / 2) + wid; + else + return left + ((right - left) - imax) / 2 + wid; + } + } + + /** + * Get the leftmost position that should be exposed for horizontal + * scrolling on the specified line. + */ + public float getLineLeft(int line) { + int dir = getParagraphDirection(line); + Alignment align = getParagraphAlignment(line); + + if (align == Alignment.ALIGN_NORMAL) { + if (dir == DIR_RIGHT_TO_LEFT) + return getParagraphRight(line) - getLineMax(line); + else + return 0; + } else if (align == Alignment.ALIGN_OPPOSITE) { + if (dir == DIR_RIGHT_TO_LEFT) + return 0; + else + return mWidth - getLineMax(line); + } else { /* align == Alignment.ALIGN_CENTER */ + int left = getParagraphLeft(line); + int right = getParagraphRight(line); + int max = ((int) getLineMax(line)) & ~1; + + return left + ((right - left) - max) / 2; + } + } + + /** + * Get the rightmost position that should be exposed for horizontal + * scrolling on the specified line. + */ + public float getLineRight(int line) { + int dir = getParagraphDirection(line); + Alignment align = getParagraphAlignment(line); + + if (align == Alignment.ALIGN_NORMAL) { + if (dir == DIR_RIGHT_TO_LEFT) + return mWidth; + else + return getParagraphLeft(line) + getLineMax(line); + } else if (align == Alignment.ALIGN_OPPOSITE) { + if (dir == DIR_RIGHT_TO_LEFT) + return getLineMax(line); + else + return mWidth; + } else { /* align == Alignment.ALIGN_CENTER */ + int left = getParagraphLeft(line); + int right = getParagraphRight(line); + int max = ((int) getLineMax(line)) & ~1; + + return right - ((right - left) - max) / 2; + } + } + + /** + * Gets the horizontal extent of the specified line, excluding + * trailing whitespace. + */ + public float getLineMax(int line) { + return getLineMax(line, null, false); + } + + /** + * Gets the horizontal extent of the specified line, including + * trailing whitespace. + */ + public float getLineWidth(int line) { + return getLineMax(line, null, true); + } + + private float getLineMax(int line, Object[] tabs, boolean full) { + int start = getLineStart(line); + int end; + + if (full) { + end = getLineEnd(line); + } else { + end = getLineVisibleEnd(line); + } + boolean tab = getLineContainsTab(line); + + if (tabs == null && tab && mText instanceof Spanned) { + tabs = ((Spanned) mText).getSpans(start, end, TabStopSpan.class); + } + + return measureText(mPaint, mWorkPaint, + mText, start, end, null, tab, tabs); + } + + /** + * Get the line number corresponding to the specified vertical position. + * If you ask for a position above 0, you get 0; if you ask for a position + * below the bottom of the text, you get the last line. + */ + // FIXME: It may be faster to do a linear search for layouts without many lines. + public int getLineForVertical(int vertical) { + int high = getLineCount(), low = -1, guess; + + while (high - low > 1) { + guess = (high + low) / 2; + + if (getLineTop(guess) > vertical) + high = guess; + else + low = guess; + } + + if (low < 0) + return 0; + else + return low; + } + + /** + * Get the line number on which the specified text offset appears. + * If you ask for a position before 0, you get 0; if you ask for a position + * beyond the end of the text, you get the last line. + */ + public int getLineForOffset(int offset) { + int high = getLineCount(), low = -1, guess; + + while (high - low > 1) { + guess = (high + low) / 2; + + if (getLineStart(guess) > offset) + high = guess; + else + low = guess; + } + + if (low < 0) + return 0; + else + return low; + } + + /** + * Get the character offset on the specfied line whose position is + * closest to the specified horizontal position. + */ + public int getOffsetForHorizontal(int line, float horiz) { + int max = getLineEnd(line) - 1; + int min = getLineStart(line); + Directions dirs = getLineDirections(line); + + if (line == getLineCount() - 1) + max++; + + int best = min; + float bestdist = Math.abs(getPrimaryHorizontal(best) - horiz); + + int here = min; + for (int i = 0; i < dirs.mDirections.length; i++) { + int there = here + dirs.mDirections[i]; + int swap = ((i & 1) == 0) ? 1 : -1; + + if (there > max) + there = max; + + int high = there - 1 + 1, low = here + 1 - 1, guess; + + while (high - low > 1) { + guess = (high + low) / 2; + int adguess = getOffsetAtStartOf(guess); + + if (getPrimaryHorizontal(adguess) * swap >= horiz * swap) + high = guess; + else + low = guess; + } + + if (low < here + 1) + low = here + 1; + + if (low < there) { + low = getOffsetAtStartOf(low); + + float dist = Math.abs(getPrimaryHorizontal(low) - horiz); + + int aft = TextUtils.getOffsetAfter(mText, low); + if (aft < there) { + float other = Math.abs(getPrimaryHorizontal(aft) - horiz); + + if (other < dist) { + dist = other; + low = aft; + } + } + + if (dist < bestdist) { + bestdist = dist; + best = low; + } + } + + float dist = Math.abs(getPrimaryHorizontal(here) - horiz); + + if (dist < bestdist) { + bestdist = dist; + best = here; + } + + here = there; + } + + float dist = Math.abs(getPrimaryHorizontal(max) - horiz); + + if (dist < bestdist) { + bestdist = dist; + best = max; + } + + return best; + } + + /** + * Return the text offset after the last character on the specified line. + */ + public final int getLineEnd(int line) { + return getLineStart(line + 1); + } + + /** + * Return the text offset after the last visible character (so whitespace + * is not counted) on the specified line. + */ + public int getLineVisibleEnd(int line) { + return getLineVisibleEnd(line, getLineStart(line), getLineStart(line+1)); + } + + private int getLineVisibleEnd(int line, int start, int end) { + if (Config.DEBUG) { + Assert.assertTrue(getLineStart(line) == start && getLineStart(line+1) == end); + } + + CharSequence text = mText; + char ch; + if (line == getLineCount() - 1) { + return end; + } + + for (; end > start; end--) { + ch = text.charAt(end - 1); + + if (ch == '\n') { + return end - 1; + } + + if (ch != ' ' && ch != '\t') { + break; + } + + } + + return end; + } + + /** + * Return the vertical position of the bottom of the specified line. + */ + public final int getLineBottom(int line) { + return getLineTop(line + 1); + } + + /** + * Return the vertical position of the baseline of the specified line. + */ + public final int getLineBaseline(int line) { + // getLineTop(line+1) == getLineTop(line) + return getLineTop(line+1) - getLineDescent(line); + } + + /** + * Get the ascent of the text on the specified line. + * The return value is negative to match the Paint.ascent() convention. + */ + public final int getLineAscent(int line) { + // getLineTop(line+1) - getLineDescent(line) == getLineBaseLine(line) + return getLineTop(line) - (getLineTop(line+1) - getLineDescent(line)); + } + + /** + * Return the text offset that would be reached by moving left + * (possibly onto another line) from the specified offset. + */ + public int getOffsetToLeftOf(int offset) { + int line = getLineForOffset(offset); + int start = getLineStart(line); + int end = getLineEnd(line); + Directions dirs = getLineDirections(line); + + if (line != getLineCount() - 1) + end--; + + float horiz = getPrimaryHorizontal(offset); + + int best = offset; + float besth = Integer.MIN_VALUE; + int candidate; + + candidate = TextUtils.getOffsetBefore(mText, offset); + if (candidate >= start && candidate <= end) { + float h = getPrimaryHorizontal(candidate); + + if (h < horiz && h > besth) { + best = candidate; + besth = h; + } + } + + candidate = TextUtils.getOffsetAfter(mText, offset); + if (candidate >= start && candidate <= end) { + float h = getPrimaryHorizontal(candidate); + + if (h < horiz && h > besth) { + best = candidate; + besth = h; + } + } + + int here = start; + for (int i = 0; i < dirs.mDirections.length; i++) { + int there = here + dirs.mDirections[i]; + if (there > end) + there = end; + + float h = getPrimaryHorizontal(here); + + if (h < horiz && h > besth) { + best = here; + besth = h; + } + + candidate = TextUtils.getOffsetAfter(mText, here); + if (candidate >= start && candidate <= end) { + h = getPrimaryHorizontal(candidate); + + if (h < horiz && h > besth) { + best = candidate; + besth = h; + } + } + + candidate = TextUtils.getOffsetBefore(mText, there); + if (candidate >= start && candidate <= end) { + h = getPrimaryHorizontal(candidate); + + if (h < horiz && h > besth) { + best = candidate; + besth = h; + } + } + + here = there; + } + + float h = getPrimaryHorizontal(end); + + if (h < horiz && h > besth) { + best = end; + besth = h; + } + + if (best != offset) + return best; + + int dir = getParagraphDirection(line); + + if (dir > 0) { + if (line == 0) + return best; + else + return getOffsetForHorizontal(line - 1, 10000); + } else { + if (line == getLineCount() - 1) + return best; + else + return getOffsetForHorizontal(line + 1, 10000); + } + } + + /** + * Return the text offset that would be reached by moving right + * (possibly onto another line) from the specified offset. + */ + public int getOffsetToRightOf(int offset) { + int line = getLineForOffset(offset); + int start = getLineStart(line); + int end = getLineEnd(line); + Directions dirs = getLineDirections(line); + + if (line != getLineCount() - 1) + end--; + + float horiz = getPrimaryHorizontal(offset); + + int best = offset; + float besth = Integer.MAX_VALUE; + int candidate; + + candidate = TextUtils.getOffsetBefore(mText, offset); + if (candidate >= start && candidate <= end) { + float h = getPrimaryHorizontal(candidate); + + if (h > horiz && h < besth) { + best = candidate; + besth = h; + } + } + + candidate = TextUtils.getOffsetAfter(mText, offset); + if (candidate >= start && candidate <= end) { + float h = getPrimaryHorizontal(candidate); + + if (h > horiz && h < besth) { + best = candidate; + besth = h; + } + } + + int here = start; + for (int i = 0; i < dirs.mDirections.length; i++) { + int there = here + dirs.mDirections[i]; + if (there > end) + there = end; + + float h = getPrimaryHorizontal(here); + + if (h > horiz && h < besth) { + best = here; + besth = h; + } + + candidate = TextUtils.getOffsetAfter(mText, here); + if (candidate >= start && candidate <= end) { + h = getPrimaryHorizontal(candidate); + + if (h > horiz && h < besth) { + best = candidate; + besth = h; + } + } + + candidate = TextUtils.getOffsetBefore(mText, there); + if (candidate >= start && candidate <= end) { + h = getPrimaryHorizontal(candidate); + + if (h > horiz && h < besth) { + best = candidate; + besth = h; + } + } + + here = there; + } + + float h = getPrimaryHorizontal(end); + + if (h > horiz && h < besth) { + best = end; + besth = h; + } + + if (best != offset) + return best; + + int dir = getParagraphDirection(line); + + if (dir > 0) { + if (line == getLineCount() - 1) + return best; + else + return getOffsetForHorizontal(line + 1, -10000); + } else { + if (line == 0) + return best; + else + return getOffsetForHorizontal(line - 1, -10000); + } + } + + private int getOffsetAtStartOf(int offset) { + if (offset == 0) + return 0; + + CharSequence text = mText; + char c = text.charAt(offset); + + if (c >= '\uDC00' && c <= '\uDFFF') { + char c1 = text.charAt(offset - 1); + + if (c1 >= '\uD800' && c1 <= '\uDBFF') + offset -= 1; + } + + if (mSpannedText) { + ReplacementSpan[] spans = ((Spanned) text).getSpans(offset, offset, + ReplacementSpan.class); + + for (int i = 0; i < spans.length; i++) { + int start = ((Spanned) text).getSpanStart(spans[i]); + int end = ((Spanned) text).getSpanEnd(spans[i]); + + if (start < offset && end > offset) + offset = start; + } + } + + return offset; + } + + /** + * Fills in the specified Path with a representation of a cursor + * at the specified offset. This will often be a vertical line + * but can be multiple discontinous lines in text with multiple + * directionalities. + */ + public void getCursorPath(int point, Path dest, + CharSequence editingBuffer) { + dest.reset(); + + int line = getLineForOffset(point); + int top = getLineTop(line); + int bottom = getLineTop(line+1); + + float h1 = getPrimaryHorizontal(point) - 0.5f; + float h2 = getSecondaryHorizontal(point) - 0.5f; + + int caps = TextKeyListener.getMetaState(editingBuffer, + KeyEvent.META_SHIFT_ON); + int fn = TextKeyListener.getMetaState(editingBuffer, + KeyEvent.META_ALT_ON); + int dist = 0; + + if (caps != 0 || fn != 0) { + dist = (bottom - top) >> 2; + + if (fn != 0) + top += dist; + if (caps != 0) + bottom -= dist; + } + + if (h1 < 0.5f) + h1 = 0.5f; + if (h2 < 0.5f) + h2 = 0.5f; + + if (h1 == h2) { + dest.moveTo(h1, top); + dest.lineTo(h1, bottom); + } else { + dest.moveTo(h1, top); + dest.lineTo(h1, (top + bottom) >> 1); + + dest.moveTo(h2, (top + bottom) >> 1); + dest.lineTo(h2, bottom); + } + + if (caps == 2) { + dest.moveTo(h2, bottom); + dest.lineTo(h2 - dist, bottom + dist); + dest.lineTo(h2, bottom); + dest.lineTo(h2 + dist, bottom + dist); + } else if (caps == 1) { + dest.moveTo(h2, bottom); + dest.lineTo(h2 - dist, bottom + dist); + + dest.moveTo(h2 - dist, bottom + dist - 0.5f); + dest.lineTo(h2 + dist, bottom + dist - 0.5f); + + dest.moveTo(h2 + dist, bottom + dist); + dest.lineTo(h2, bottom); + } + + if (fn == 2) { + dest.moveTo(h1, top); + dest.lineTo(h1 - dist, top - dist); + dest.lineTo(h1, top); + dest.lineTo(h1 + dist, top - dist); + } else if (fn == 1) { + dest.moveTo(h1, top); + dest.lineTo(h1 - dist, top - dist); + + dest.moveTo(h1 - dist, top - dist + 0.5f); + dest.lineTo(h1 + dist, top - dist + 0.5f); + + dest.moveTo(h1 + dist, top - dist); + dest.lineTo(h1, top); + } + } + + private void addSelection(int line, int start, int end, + int top, int bottom, Path dest) { + int linestart = getLineStart(line); + int lineend = getLineEnd(line); + Directions dirs = getLineDirections(line); + + if (lineend > linestart && mText.charAt(lineend - 1) == '\n') + lineend--; + + int here = linestart; + for (int i = 0; i < dirs.mDirections.length; i++) { + int there = here + dirs.mDirections[i]; + if (there > lineend) + there = lineend; + + if (start <= there && end >= here) { + int st = Math.max(start, here); + int en = Math.min(end, there); + + if (st != en) { + float h1 = getHorizontal(st, false, false, line); + float h2 = getHorizontal(en, true, false, line); + + dest.addRect(h1, top, h2, bottom, Path.Direction.CW); + } + } + + here = there; + } + } + + /** + * Fills in the specified Path with a representation of a highlight + * between the specified offsets. This will often be a rectangle + * or a potentially discontinuous set of rectangles. If the start + * and end are the same, the returned path is empty. + */ + public void getSelectionPath(int start, int end, Path dest) { + dest.reset(); + + if (start == end) + return; + + if (end < start) { + int temp = end; + end = start; + start = temp; + } + + int startline = getLineForOffset(start); + int endline = getLineForOffset(end); + + int top = getLineTop(startline); + int bottom = getLineBottom(endline); + + if (startline == endline) { + addSelection(startline, start, end, top, bottom, dest); + } else { + final float width = mWidth; + + addSelection(startline, start, getLineEnd(startline), + top, getLineBottom(startline), dest); + + if (getParagraphDirection(startline) == DIR_RIGHT_TO_LEFT) + dest.addRect(getLineLeft(startline), top, + 0, getLineBottom(startline), Path.Direction.CW); + else + dest.addRect(getLineRight(startline), top, + width, getLineBottom(startline), Path.Direction.CW); + + for (int i = startline + 1; i < endline; i++) { + top = getLineTop(i); + bottom = getLineBottom(i); + dest.addRect(0, top, width, bottom, Path.Direction.CW); + } + + top = getLineTop(endline); + bottom = getLineBottom(endline); + + addSelection(endline, getLineStart(endline), end, + top, bottom, dest); + + if (getParagraphDirection(endline) == DIR_RIGHT_TO_LEFT) + dest.addRect(width, top, getLineRight(endline), bottom, Path.Direction.CW); + else + dest.addRect(0, top, getLineLeft(endline), bottom, Path.Direction.CW); + } + } + + /** + * Get the alignment of the specified paragraph, taking into account + * markup attached to it. + */ + public final Alignment getParagraphAlignment(int line) { + Alignment align = mAlignment; + + if (mSpannedText) { + Spanned sp = (Spanned) mText; + AlignmentSpan[] spans = sp.getSpans(getLineStart(line), + getLineEnd(line), + AlignmentSpan.class); + + int spanLength = spans.length; + if (spanLength > 0) { + align = spans[spanLength-1].getAlignment(); + } + } + + return align; + } + + /** + * Get the left edge of the specified paragraph, inset by left margins. + */ + public final int getParagraphLeft(int line) { + int dir = getParagraphDirection(line); + + int left = 0; + + boolean par = false; + int off = getLineStart(line); + if (off == 0 || mText.charAt(off - 1) == '\n') + par = true; + + if (dir == DIR_LEFT_TO_RIGHT) { + if (mSpannedText) { + Spanned sp = (Spanned) mText; + LeadingMarginSpan[] spans = sp.getSpans(getLineStart(line), + getLineEnd(line), + LeadingMarginSpan.class); + + for (int i = 0; i < spans.length; i++) { + left += spans[i].getLeadingMargin(par); + } + } + } + + return left; + } + + /** + * Get the right edge of the specified paragraph, inset by right margins. + */ + public final int getParagraphRight(int line) { + int dir = getParagraphDirection(line); + + int right = mWidth; + + boolean par = false; + int off = getLineStart(line); + if (off == 0 || mText.charAt(off - 1) == '\n') + par = true; + + + if (dir == DIR_RIGHT_TO_LEFT) { + if (mSpannedText) { + Spanned sp = (Spanned) mText; + LeadingMarginSpan[] spans = sp.getSpans(getLineStart(line), + getLineEnd(line), + LeadingMarginSpan.class); + + for (int i = 0; i < spans.length; i++) { + right -= spans[i].getLeadingMargin(par); + } + } + } + + return right; + } + + private static void drawText(Canvas canvas, + CharSequence text, int start, int end, + int dir, Directions directions, + float x, int top, int y, int bottom, + TextPaint paint, + TextPaint workPaint, + boolean hasTabs, Object[] parspans) { + char[] buf; + if (!hasTabs) { + if (directions == DIRS_ALL_LEFT_TO_RIGHT) { + if (Config.DEBUG) { + Assert.assertTrue(DIR_LEFT_TO_RIGHT == dir); + } + Styled.drawText(canvas, text, start, end, dir, false, x, top, y, bottom, paint, workPaint, false); + return; + } + buf = null; + } else { + buf = TextUtils.obtain(end - start); + TextUtils.getChars(text, start, end, buf, 0); + } + + float h = 0; + + int here = 0; + for (int i = 0; i < directions.mDirections.length; i++) { + int there = here + directions.mDirections[i]; + if (there > end - start) + there = end - start; + + int segstart = here; + for (int j = hasTabs ? here : there; j <= there; j++) { + if (j == there || buf[j] == '\t') { + h += Styled.drawText(canvas, text, + start + segstart, start + j, + dir, (i & 1) != 0, x + h, + top, y, bottom, paint, workPaint, + start + j != end); + + if (j != there && buf[j] == '\t') + h = dir * nextTab(text, start, end, h * dir, parspans); + + segstart = j + 1; + } + } + + here = there; + } + + if (hasTabs) + TextUtils.recycle(buf); + } + + private static float measureText(TextPaint paint, + TextPaint workPaint, + CharSequence text, + int start, int offset, int end, + int dir, Directions directions, + boolean trailing, boolean alt, + boolean hasTabs, Object[] tabs) { + char[] buf = null; + + if (hasTabs) { + buf = TextUtils.obtain(end - start); + TextUtils.getChars(text, start, end, buf, 0); + } + + float h = 0; + + if (alt) { + if (dir == DIR_RIGHT_TO_LEFT) + trailing = !trailing; + } + + int here = 0; + for (int i = 0; i < directions.mDirections.length; i++) { + if (alt) + trailing = !trailing; + + int there = here + directions.mDirections[i]; + if (there > end - start) + there = end - start; + + int segstart = here; + for (int j = hasTabs ? here : there; j <= there; j++) { + if (j == there || buf[j] == '\t') { + float segw; + + if (offset < start + j || + (trailing && offset <= start + j)) { + if (dir == DIR_LEFT_TO_RIGHT && (i & 1) == 0) { + h += Styled.measureText(paint, workPaint, text, + start + segstart, offset, + null); + return h; + } + + if (dir == DIR_RIGHT_TO_LEFT && (i & 1) != 0) { + h -= Styled.measureText(paint, workPaint, text, + start + segstart, offset, + null); + return h; + } + } + + segw = Styled.measureText(paint, workPaint, text, + start + segstart, start + j, + null); + + if (offset < start + j || + (trailing && offset <= start + j)) { + if (dir == DIR_LEFT_TO_RIGHT) { + h += segw - Styled.measureText(paint, workPaint, + text, + start + segstart, + offset, null); + return h; + } + + if (dir == DIR_RIGHT_TO_LEFT) { + h -= segw - Styled.measureText(paint, workPaint, + text, + start + segstart, + offset, null); + return h; + } + } + + if (dir == DIR_RIGHT_TO_LEFT) + h -= segw; + else + h += segw; + + if (j != there && buf[j] == '\t') { + if (offset == start + j) + return h; + + h = dir * nextTab(text, start, end, h * dir, tabs); + } + + segstart = j + 1; + } + } + + here = there; + } + + if (hasTabs) + TextUtils.recycle(buf); + + return h; + } + + /* package */ static float measureText(TextPaint paint, + TextPaint workPaint, + CharSequence text, + int start, int end, + Paint.FontMetricsInt fm, + boolean hasTabs, Object[] tabs) { + char[] buf = null; + + if (hasTabs) { + buf = TextUtils.obtain(end - start); + TextUtils.getChars(text, start, end, buf, 0); + } + + int len = end - start; + + int here = 0; + float h = 0; + int ab = 0, be = 0; + int top = 0, bot = 0; + + if (fm != null) { + fm.ascent = 0; + fm.descent = 0; + } + + for (int i = hasTabs ? 0 : len; i <= len; i++) { + if (i == len || buf[i] == '\t') { + workPaint.baselineShift = 0; + + h += Styled.measureText(paint, workPaint, text, + start + here, start + i, + fm); + + if (fm != null) { + if (workPaint.baselineShift < 0) { + fm.ascent += workPaint.baselineShift; + fm.top += workPaint.baselineShift; + } else { + fm.descent += workPaint.baselineShift; + fm.bottom += workPaint.baselineShift; + } + } + + if (i != len) + h = nextTab(text, start, end, h, tabs); + + if (fm != null) { + if (fm.ascent < ab) { + ab = fm.ascent; + } + if (fm.descent > be) { + be = fm.descent; + } + + if (fm.top < top) { + top = fm.top; + } + if (fm.bottom > bot) { + bot = fm.bottom; + } + } + + here = i + 1; + } + } + + if (fm != null) { + fm.ascent = ab; + fm.descent = be; + fm.top = top; + fm.bottom = bot; + } + + if (hasTabs) + TextUtils.recycle(buf); + + return h; + } + + /* package */ static float nextTab(CharSequence text, int start, int end, + float h, Object[] tabs) { + float nh = Float.MAX_VALUE; + boolean alltabs = false; + + if (text instanceof Spanned) { + if (tabs == null) { + tabs = ((Spanned) text).getSpans(start, end, TabStopSpan.class); + alltabs = true; + } + + for (int i = 0; i < tabs.length; i++) { + if (!alltabs) { + if (!(tabs[i] instanceof TabStopSpan)) + continue; + } + + int where = ((TabStopSpan) tabs[i]).getTabStop(); + + if (where < nh && where > h) + nh = where; + } + + if (nh != Float.MAX_VALUE) + return nh; + } + + return ((int) ((h + TAB_INCREMENT) / TAB_INCREMENT)) * TAB_INCREMENT; + } + + protected final boolean isSpanned() { + return mSpannedText; + } + + private void ellipsize(int start, int end, int line, + char[] dest, int destoff) { + int ellipsisCount = getEllipsisCount(line); + + if (ellipsisCount == 0) { + return; + } + + int ellipsisStart = getEllipsisStart(line); + int linestart = getLineStart(line); + + for (int i = ellipsisStart; i < ellipsisStart + ellipsisCount; i++) { + char c; + + if (i == ellipsisStart) { + c = '\u2026'; // ellipsis + } else { + c = '\uFEFF'; // 0-width space + } + + int a = i + linestart; + + if (a >= start && a < end) { + dest[destoff + a - start] = c; + } + } + } + + /** + * Stores information about bidirectional (left-to-right or right-to-left) + * text within the layout of a line. TODO: This work is not complete + * or correct and will be fleshed out in a later revision. + */ + public static class Directions { + private short[] mDirections; + + /* package */ Directions(short[] dirs) { + mDirections = dirs; + } + } + + /** + * Return the offset of the first character to be ellipsized away, + * relative to the start of the line. (So 0 if the beginning of the + * line is ellipsized, not getLineStart().) + */ + public abstract int getEllipsisStart(int line); + /** + * Returns the number of characters to be ellipsized away, or 0 if + * no ellipsis is to take place. + */ + public abstract int getEllipsisCount(int line); + + /* package */ static class Ellipsizer implements CharSequence, GetChars { + /* package */ CharSequence mText; + /* package */ Layout mLayout; + /* package */ int mWidth; + /* package */ TextUtils.TruncateAt mMethod; + + public Ellipsizer(CharSequence s) { + mText = s; + } + + public char charAt(int off) { + char[] buf = TextUtils.obtain(1); + getChars(off, off + 1, buf, 0); + char ret = buf[0]; + + TextUtils.recycle(buf); + return ret; + } + + public void getChars(int start, int end, char[] dest, int destoff) { + int line1 = mLayout.getLineForOffset(start); + int line2 = mLayout.getLineForOffset(end); + + TextUtils.getChars(mText, start, end, dest, destoff); + + for (int i = line1; i <= line2; i++) { + mLayout.ellipsize(start, end, i, dest, destoff); + } + } + + public int length() { + return mText.length(); + } + + public CharSequence subSequence(int start, int end) { + char[] s = new char[end - start]; + getChars(start, end, s, 0); + return new String(s); + } + + public String toString() { + char[] s = new char[length()]; + getChars(0, length(), s, 0); + return new String(s); + } + + } + + /* package */ static class SpannedEllipsizer + extends Ellipsizer implements Spanned { + private Spanned mSpanned; + + public SpannedEllipsizer(CharSequence display) { + super(display); + mSpanned = (Spanned) display; + } + + public <T> T[] getSpans(int start, int end, Class<T> type) { + return mSpanned.getSpans(start, end, type); + } + + public int getSpanStart(Object tag) { + return mSpanned.getSpanStart(tag); + } + + public int getSpanEnd(Object tag) { + return mSpanned.getSpanEnd(tag); + } + + public int getSpanFlags(Object tag) { + return mSpanned.getSpanFlags(tag); + } + + public int nextSpanTransition(int start, int limit, Class type) { + return mSpanned.nextSpanTransition(start, limit, type); + } + + public CharSequence subSequence(int start, int end) { + char[] s = new char[end - start]; + getChars(start, end, s, 0); + + SpannableString ss = new SpannableString(new String(s)); + TextUtils.copySpansFrom(mSpanned, start, end, Object.class, ss, 0); + return ss; + } + } + + private CharSequence mText; + private TextPaint mPaint; + /* package */ TextPaint mWorkPaint; + private int mWidth; + private Alignment mAlignment = Alignment.ALIGN_NORMAL; + private float mSpacingMult; + private float mSpacingAdd; + private static Rect sTempRect = new Rect(); + private boolean mSpannedText; + + public static final int DIR_LEFT_TO_RIGHT = 1; + public static final int DIR_RIGHT_TO_LEFT = -1; + + public enum Alignment { + ALIGN_NORMAL, + ALIGN_OPPOSITE, + ALIGN_CENTER, + // XXX ALIGN_LEFT, + // XXX ALIGN_RIGHT, + } + + private static final int TAB_INCREMENT = 20; + + /* package */ static final Directions DIRS_ALL_LEFT_TO_RIGHT = + new Directions(new short[] { 32767 }); + /* package */ static final Directions DIRS_ALL_RIGHT_TO_LEFT = + new Directions(new short[] { 0, 32767 }); + +} + diff --git a/core/java/android/text/LoginFilter.java b/core/java/android/text/LoginFilter.java new file mode 100644 index 0000000..dd2d77f --- /dev/null +++ b/core/java/android/text/LoginFilter.java @@ -0,0 +1,206 @@ +/* + * Copyright (C) 2006 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; + +/** + * Abstract class for filtering login-related text (user names and passwords) + * + */ +public abstract class LoginFilter implements InputFilter { + private boolean mAppendInvalid; // whether to append or ignore invalid characters + /** + * Base constructor for LoginFilter + * @param appendInvalid whether or not to append invalid characters. + */ + LoginFilter(boolean appendInvalid) { + mAppendInvalid = appendInvalid; + } + + /** + * Default constructor for LoginFilter doesn't append invalid characters. + */ + LoginFilter() { + mAppendInvalid = false; + } + + /** + * This method is called when the buffer is going to replace the + * range <code>dstart … dend</code> of <code>dest</code> + * with the new text from the range <code>start … end</code> + * of <code>source</code>. Returns the CharSequence that we want + * placed there instead, including an empty string + * if appropriate, or <code>null</code> to accept the original + * replacement. Be careful to not to reject 0-length replacements, + * as this is what happens when you delete text. + */ + public CharSequence filter(CharSequence source, int start, int end, + Spanned dest, int dstart, int dend) { + char[] out = new char[end - start]; // reserve enough space for whole string + int outidx = 0; + boolean changed = false; + + onStart(); + + // Scan through beginning characters in dest, calling onInvalidCharacter() + // for each invalid character. + for (int i = 0; i < dstart; i++) { + char c = dest.charAt(i); + if (!isAllowed(c)) onInvalidCharacter(c); + } + + // Scan through changed characters rejecting disallowed chars + for (int i = start; i < end; i++) { + char c = source.charAt(i); + if (isAllowed(c)) { + // Character allowed. Add it to the sequence. + out[outidx++] = c; + } else { + if (mAppendInvalid) out[outidx++] = c; + else changed = true; // we changed the original string + onInvalidCharacter(c); + } + } + + // Scan through remaining characters in dest, calling onInvalidCharacter() + // for each invalid character. + for (int i = dend; i < dest.length(); i++) { + char c = dest.charAt(i); + if (!isAllowed(c)) onInvalidCharacter(c); + } + + onStop(); + + return changed ? new String(out, 0, outidx) : null; + } + + /** + * Called when we start processing filter. + */ + public void onStart() { + + } + + /** + * Called whenever we encounter an invalid character. + * @param c the invalid character + */ + public void onInvalidCharacter(char c) { + + } + + /** + * Called when we're done processing filter + */ + public void onStop() { + + } + + /** + * Returns whether or not we allow character c. + * Subclasses must override this method. + */ + public abstract boolean isAllowed(char c); + + /** + * This filter rejects characters in the user name that are not compatible with GMail + * account creation. It prevents the user from entering user names with characters other than + * [a-zA-Z0-9.]. + * + */ + public static class UsernameFilterGMail extends LoginFilter { + + public UsernameFilterGMail() { + super(false); + } + + public UsernameFilterGMail(boolean appendInvalid) { + super(appendInvalid); + } + + @Override + public boolean isAllowed(char c) { + // Allow [a-zA-Z0-9@.] + if ('0' <= c && c <= '9') + return true; + if ('a' <= c && c <= 'z') + return true; + if ('A' <= c && c <= 'Z') + return true; + if ('.' == c) + return true; + return false; + } + } + + /** + * This filter rejects characters in the user name that are not compatible with Google login. + * It is slightly less restrictive than the above filter in that it allows [a-zA-Z0-9._-]. + * + */ + public static class UsernameFilterGeneric extends LoginFilter { + private static final String mAllowed = "@_-."; // Additional characters + + public UsernameFilterGeneric() { + super(false); + } + + public UsernameFilterGeneric(boolean appendInvalid) { + super(appendInvalid); + } + + @Override + public boolean isAllowed(char c) { + // Allow [a-zA-Z0-9@.] + if ('0' <= c && c <= '9') + return true; + if ('a' <= c && c <= 'z') + return true; + if ('A' <= c && c <= 'Z') + return true; + if (mAllowed.indexOf(c) != -1) + return true; + return false; + } + } + + /** + * This filter is compatible with GMail passwords which restricts characters to + * the Latin-1 (ISO8859-1) char set. + * + */ + public static class PasswordFilterGMail extends LoginFilter { + + public PasswordFilterGMail() { + super(false); + } + + public PasswordFilterGMail(boolean appendInvalid) { + super(appendInvalid); + } + + // We should reject anything not in the Latin-1 (ISO8859-1) charset + @Override + public boolean isAllowed(char c) { + if (32 <= c && c <= 127) + return true; // standard charset + // if (128 <= c && c <= 159) return true; // nonstandard (Windows(TM)(R)) charset + if (160 <= c && c <= 255) + return true; // extended charset + return false; + } + } +} diff --git a/core/java/android/text/PackedIntVector.java b/core/java/android/text/PackedIntVector.java new file mode 100644 index 0000000..d87f600 --- /dev/null +++ b/core/java/android/text/PackedIntVector.java @@ -0,0 +1,368 @@ +/* + * Copyright (C) 2007 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; + +import com.android.internal.util.ArrayUtils; + + +/** + * PackedIntVector stores a two-dimensional array of integers, + * optimized for inserting and deleting rows and for + * offsetting the values in segments of a given column. + */ +class PackedIntVector { + private final int mColumns; + private int mRows; + + private int mRowGapStart; + private int mRowGapLength; + + private int[] mValues; + private int[] mValueGap; // starts, followed by lengths + + /** + * Creates a new PackedIntVector with the specified width and + * a height of 0. + * + * @param columns the width of the PackedIntVector. + */ + public PackedIntVector(int columns) { + mColumns = columns; + mRows = 0; + + mRowGapStart = 0; + mRowGapLength = mRows; + + mValues = null; + mValueGap = new int[2 * columns]; + } + + /** + * Returns the value at the specified row and column. + * + * @param row the index of the row to return. + * @param column the index of the column to return. + * + * @return the value stored at the specified position. + * + * @throws IndexOutOfBoundsException if the row is out of range + * (row < 0 || row >= size()) or the column is out of range + * (column < 0 || column >= width()). + */ + public int getValue(int row, int column) { + final int columns = mColumns; + + if (((row | column) < 0) || (row >= size()) || (column >= columns)) { + throw new IndexOutOfBoundsException(row + ", " + column); + } + + if (row >= mRowGapStart) { + row += mRowGapLength; + } + + int value = mValues[row * columns + column]; + + int[] valuegap = mValueGap; + if (row >= valuegap[column]) { + value += valuegap[column + columns]; + } + + return value; + } + + /** + * Sets the value at the specified row and column. + * + * @param row the index of the row to set. + * @param column the index of the column to set. + * + * @throws IndexOutOfBoundsException if the row is out of range + * (row < 0 || row >= size()) or the column is out of range + * (column < 0 || column >= width()). + */ + public void setValue(int row, int column, int value) { + if (((row | column) < 0) || (row >= size()) || (column >= mColumns)) { + throw new IndexOutOfBoundsException(row + ", " + column); + } + + if (row >= mRowGapStart) { + row += mRowGapLength; + } + + int[] valuegap = mValueGap; + if (row >= valuegap[column]) { + value -= valuegap[column + mColumns]; + } + + mValues[row * mColumns + column] = value; + } + + /** + * Sets the value at the specified row and column. + * Private internal version: does not check args. + * + * @param row the index of the row to set. + * @param column the index of the column to set. + * + */ + private void setValueInternal(int row, int column, int value) { + if (row >= mRowGapStart) { + row += mRowGapLength; + } + + int[] valuegap = mValueGap; + if (row >= valuegap[column]) { + value -= valuegap[column + mColumns]; + } + + mValues[row * mColumns + column] = value; + } + + + /** + * Increments all values in the specified column whose row >= the + * specified row by the specified delta. + * + * @param startRow the row at which to begin incrementing. + * This may be == size(), which case there is no effect. + * @param column the index of the column to set. + * + * @throws IndexOutOfBoundsException if the row is out of range + * (startRow < 0 || startRow > size()) or the column + * is out of range (column < 0 || column >= width()). + */ + public void adjustValuesBelow(int startRow, int column, int delta) { + if (((startRow | column) < 0) || (startRow > size()) || + (column >= width())) { + throw new IndexOutOfBoundsException(startRow + ", " + column); + } + + if (startRow >= mRowGapStart) { + startRow += mRowGapLength; + } + + moveValueGapTo(column, startRow); + mValueGap[column + mColumns] += delta; + } + + /** + * Inserts a new row of values at the specified row offset. + * + * @param row the row above which to insert the new row. + * This may be == size(), which case the new row is added + * at the end. + * @param values the new values to be added. If this is null, + * a row of zeroes is added. + * + * @throws IndexOutOfBoundsException if the row is out of range + * (row < 0 || row > size()) or if the length of the + * values array is too small (values.length < width()). + */ + public void insertAt(int row, int[] values) { + if ((row < 0) || (row > size())) { + throw new IndexOutOfBoundsException("row " + row); + } + + if ((values != null) && (values.length < width())) { + throw new IndexOutOfBoundsException("value count " + values.length); + } + + moveRowGapTo(row); + + if (mRowGapLength == 0) { + growBuffer(); + } + + mRowGapStart++; + mRowGapLength--; + + if (values == null) { + for (int i = mColumns - 1; i >= 0; i--) { + setValueInternal(row, i, 0); + } + } else { + for (int i = mColumns - 1; i >= 0; i--) { + setValueInternal(row, i, values[i]); + } + } + } + + /** + * Deletes the specified number of rows starting with the specified + * row. + * + * @param row the index of the first row to be deleted. + * @param count the number of rows to delete. + * + * @throws IndexOutOfBoundsException if any of the rows to be deleted + * are out of range (row < 0 || count < 0 || + * row + count > size()). + */ + public void deleteAt(int row, int count) { + if (((row | count) < 0) || (row + count > size())) { + throw new IndexOutOfBoundsException(row + ", " + count); + } + + moveRowGapTo(row + count); + + mRowGapStart -= count; + mRowGapLength += count; + + // TODO: Reclaim memory when the new height is much smaller + // than the allocated size. + } + + /** + * Returns the number of rows in the PackedIntVector. This number + * will change as rows are inserted and deleted. + * + * @return the number of rows. + */ + public int size() { + return mRows - mRowGapLength; + } + + /** + * Returns the width of the PackedIntVector. This number is set + * at construction and will not change. + * + * @return the number of columns. + */ + public int width() { + return mColumns; + } + + /** + * Grows the value and gap arrays to be large enough to store at least + * one more than the current number of rows. + */ + private final void growBuffer() { + final int columns = mColumns; + int newsize = size() + 1; + newsize = ArrayUtils.idealIntArraySize(newsize * columns) / columns; + int[] newvalues = new int[newsize * columns]; + + final int[] valuegap = mValueGap; + final int rowgapstart = mRowGapStart; + + int after = mRows - (rowgapstart + mRowGapLength); + + if (mValues != null) { + System.arraycopy(mValues, 0, newvalues, 0, columns * rowgapstart); + System.arraycopy(mValues, (mRows - after) * columns, + newvalues, (newsize - after) * columns, + after * columns); + } + + for (int i = 0; i < columns; i++) { + if (valuegap[i] >= rowgapstart) { + valuegap[i] += newsize - mRows; + + if (valuegap[i] < rowgapstart) { + valuegap[i] = rowgapstart; + } + } + } + + mRowGapLength += newsize - mRows; + mRows = newsize; + mValues = newvalues; + } + + /** + * Moves the gap in the values of the specified column to begin at + * the specified row. + */ + private final void moveValueGapTo(int column, int where) { + final int[] valuegap = mValueGap; + final int[] values = mValues; + final int columns = mColumns; + + if (where == valuegap[column]) { + return; + } else if (where > valuegap[column]) { + for (int i = valuegap[column]; i < where; i++) { + values[i * columns + column] += valuegap[column + columns]; + } + } else /* where < valuegap[column] */ { + for (int i = where; i < valuegap[column]; i++) { + values[i * columns + column] -= valuegap[column + columns]; + } + } + + valuegap[column] = where; + } + + /** + * Moves the gap in the row indices to begin at the specified row. + */ + private final void moveRowGapTo(int where) { + if (where == mRowGapStart) { + return; + } else if (where > mRowGapStart) { + int moving = where + mRowGapLength - (mRowGapStart + mRowGapLength); + final int columns = mColumns; + final int[] valuegap = mValueGap; + final int[] values = mValues; + final int gapend = mRowGapStart + mRowGapLength; + + for (int i = gapend; i < gapend + moving; i++) { + int destrow = i - gapend + mRowGapStart; + + for (int j = 0; j < columns; j++) { + int val = values[i * columns+ j]; + + if (i >= valuegap[j]) { + val += valuegap[j + columns]; + } + + if (destrow >= valuegap[j]) { + val -= valuegap[j + columns]; + } + + values[destrow * columns + j] = val; + } + } + } else /* where < mRowGapStart */ { + int moving = mRowGapStart - where; + final int columns = mColumns; + final int[] valuegap = mValueGap; + final int[] values = mValues; + final int gapend = mRowGapStart + mRowGapLength; + + for (int i = where + moving - 1; i >= where; i--) { + int destrow = i - where + gapend - moving; + + for (int j = 0; j < columns; j++) { + int val = values[i * columns+ j]; + + if (i >= valuegap[j]) { + val += valuegap[j + columns]; + } + + if (destrow >= valuegap[j]) { + val -= valuegap[j + columns]; + } + + values[destrow * columns + j] = val; + } + } + } + + mRowGapStart = where; + } +} diff --git a/core/java/android/text/PackedObjectVector.java b/core/java/android/text/PackedObjectVector.java new file mode 100644 index 0000000..a29df09 --- /dev/null +++ b/core/java/android/text/PackedObjectVector.java @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2006 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; + +import com.android.internal.util.ArrayUtils; + +class PackedObjectVector<E> +{ + private int mColumns; + private int mRows; + + private int mRowGapStart; + private int mRowGapLength; + + private Object[] mValues; + + public + PackedObjectVector(int columns) + { + mColumns = columns; + mRows = ArrayUtils.idealIntArraySize(0) / mColumns; + + mRowGapStart = 0; + mRowGapLength = mRows; + + mValues = new Object[mRows * mColumns]; + } + + public E + getValue(int row, int column) + { + if (row >= mRowGapStart) + row += mRowGapLength; + + Object value = mValues[row * mColumns + column]; + + return (E) value; + } + + public void + setValue(int row, int column, E value) + { + if (row >= mRowGapStart) + row += mRowGapLength; + + mValues[row * mColumns + column] = value; + } + + public void + insertAt(int row, E[] values) + { + moveRowGapTo(row); + + if (mRowGapLength == 0) + growBuffer(); + + mRowGapStart++; + mRowGapLength--; + + if (values == null) + for (int i = 0; i < mColumns; i++) + setValue(row, i, null); + else + for (int i = 0; i < mColumns; i++) + setValue(row, i, values[i]); + } + + public void + deleteAt(int row, int count) + { + moveRowGapTo(row + count); + + mRowGapStart -= count; + mRowGapLength += count; + + if (mRowGapLength > size() * 2) + { + // dump(); + // growBuffer(); + } + } + + public int + size() + { + return mRows - mRowGapLength; + } + + public int + width() + { + return mColumns; + } + + private void + growBuffer() + { + int newsize = size() + 1; + newsize = ArrayUtils.idealIntArraySize(newsize * mColumns) / mColumns; + Object[] newvalues = new Object[newsize * mColumns]; + + int after = mRows - (mRowGapStart + mRowGapLength); + + System.arraycopy(mValues, 0, newvalues, 0, mColumns * mRowGapStart); + System.arraycopy(mValues, (mRows - after) * mColumns, newvalues, (newsize - after) * mColumns, after * mColumns); + + mRowGapLength += newsize - mRows; + mRows = newsize; + mValues = newvalues; + } + + private void + moveRowGapTo(int where) + { + if (where == mRowGapStart) + return; + + if (where > mRowGapStart) + { + int moving = where + mRowGapLength - (mRowGapStart + mRowGapLength); + + for (int i = mRowGapStart + mRowGapLength; i < mRowGapStart + mRowGapLength + moving; i++) + { + int destrow = i - (mRowGapStart + mRowGapLength) + mRowGapStart; + + for (int j = 0; j < mColumns; j++) + { + Object val = mValues[i * mColumns + j]; + + mValues[destrow * mColumns + j] = val; + } + } + } + else /* where < mRowGapStart */ + { + int moving = mRowGapStart - where; + + for (int i = where + moving - 1; i >= where; i--) + { + int destrow = i - where + mRowGapStart + mRowGapLength - moving; + + for (int j = 0; j < mColumns; j++) + { + Object val = mValues[i * mColumns + j]; + + mValues[destrow * mColumns + j] = val; + } + } + } + + mRowGapStart = where; + } + + public void // XXX + dump() + { + for (int i = 0; i < mRows; i++) + { + for (int j = 0; j < mColumns; j++) + { + Object val = mValues[i * mColumns + j]; + + if (i < mRowGapStart || i >= mRowGapStart + mRowGapLength) + System.out.print(val + " "); + else + System.out.print("(" + val + ") "); + } + + System.out.print(" << \n"); + } + + System.out.print("-----\n\n"); + } +} diff --git a/core/java/android/text/Selection.java b/core/java/android/text/Selection.java new file mode 100644 index 0000000..0f4916a --- /dev/null +++ b/core/java/android/text/Selection.java @@ -0,0 +1,426 @@ +/* + * Copyright (C) 2006 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; + + +/** + * Utility class for manipulating cursors and selections in CharSequences. + * A cursor is a selection where the start and end are at the same offset. + */ +public class Selection { + private Selection() { /* cannot be instantiated */ } + + /* + * Retrieving the selection + */ + + /** + * Return the offset of the selection anchor or cursor, or -1 if + * there is no selection or cursor. + */ + public static final int getSelectionStart(CharSequence text) { + if (text instanceof Spanned) + return ((Spanned) text).getSpanStart(SELECTION_START); + else + return -1; + } + + /** + * Return the offset of the selection edge or cursor, or -1 if + * there is no selection or cursor. + */ + public static final int getSelectionEnd(CharSequence text) { + if (text instanceof Spanned) + return ((Spanned) text).getSpanStart(SELECTION_END); + else + return -1; + } + + /* + * Setting the selection + */ + + // private static int pin(int value, int min, int max) { + // return value < min ? 0 : (value > max ? max : value); + // } + + /** + * Set the selection anchor to <code>start</code> and the selection edge + * to <code>stop</code>. + */ + public static void setSelection(Spannable text, int start, int stop) { + // int len = text.length(); + // start = pin(start, 0, len); XXX remove unless we really need it + // stop = pin(stop, 0, len); + + int ostart = getSelectionStart(text); + int oend = getSelectionEnd(text); + + if (ostart != start || oend != stop) { + text.setSpan(SELECTION_START, start, start, + Spanned.SPAN_POINT_POINT); + text.setSpan(SELECTION_END, stop, stop, + Spanned.SPAN_POINT_POINT); + } + } + + /** + * Move the cursor to offset <code>index</code>. + */ + public static final void setSelection(Spannable text, int index) { + setSelection(text, index, index); + } + + /** + * Select the entire text. + */ + public static final void selectAll(Spannable text) { + setSelection(text, 0, text.length()); + } + + /** + * Move the selection edge to offset <code>index</code>. + */ + public static final void extendSelection(Spannable text, int index) { + if (text.getSpanStart(SELECTION_END) != index) + text.setSpan(SELECTION_END, index, index, Spanned.SPAN_POINT_POINT); + } + + /** + * Remove the selection or cursor, if any, from the text. + */ + public static final void removeSelection(Spannable text) { + text.removeSpan(SELECTION_START); + text.removeSpan(SELECTION_END); + } + + /* + * Moving the selection within the layout + */ + + /** + * Move the cursor to the buffer offset physically above the current + * offset, or return false if the cursor is already on the top line. + */ + public static boolean moveUp(Spannable text, Layout layout) { + int start = getSelectionStart(text); + int end = getSelectionEnd(text); + + if (start != end) { + int min = Math.min(start, end); + int max = Math.max(start, end); + + setSelection(text, min); + + if (min == 0 && max == text.length()) { + return false; + } + + return true; + } else { + int line = layout.getLineForOffset(end); + + if (line > 0) { + int move; + + if (layout.getParagraphDirection(line) == + layout.getParagraphDirection(line - 1)) { + float h = layout.getPrimaryHorizontal(end); + move = layout.getOffsetForHorizontal(line - 1, h); + } else { + move = layout.getLineStart(line - 1); + } + + setSelection(text, move); + return true; + } + } + + return false; + } + + /** + * Move the cursor to the buffer offset physically below the current + * offset, or return false if the cursor is already on the bottom line. + */ + public static boolean moveDown(Spannable text, Layout layout) { + int start = getSelectionStart(text); + int end = getSelectionEnd(text); + + if (start != end) { + int min = Math.min(start, end); + int max = Math.max(start, end); + + setSelection(text, max); + + if (min == 0 && max == text.length()) { + return false; + } + + return true; + } else { + int line = layout.getLineForOffset(end); + + if (line < layout.getLineCount() - 1) { + int move; + + if (layout.getParagraphDirection(line) == + layout.getParagraphDirection(line + 1)) { + float h = layout.getPrimaryHorizontal(end); + move = layout.getOffsetForHorizontal(line + 1, h); + } else { + move = layout.getLineStart(line + 1); + } + + setSelection(text, move); + return true; + } + } + + return false; + } + + /** + * Move the cursor to the buffer offset physically to the left of + * the current offset, or return false if the cursor is already + * at the left edge of the line and there is not another line to move it to. + */ + public static boolean moveLeft(Spannable text, Layout layout) { + int start = getSelectionStart(text); + int end = getSelectionEnd(text); + + if (start != end) { + setSelection(text, chooseHorizontal(layout, -1, start, end)); + return true; + } else { + int to = layout.getOffsetToLeftOf(end); + + if (to != end) { + setSelection(text, to); + return true; + } + } + + return false; + } + + /** + * Move the cursor to the buffer offset physically to the right of + * the current offset, or return false if the cursor is already at + * at the right edge of the line and there is not another line + * to move it to. + */ + public static boolean moveRight(Spannable text, Layout layout) { + int start = getSelectionStart(text); + int end = getSelectionEnd(text); + + if (start != end) { + setSelection(text, chooseHorizontal(layout, 1, start, end)); + return true; + } else { + int to = layout.getOffsetToRightOf(end); + + if (to != end) { + setSelection(text, to); + return true; + } + } + + return false; + } + + /** + * Move the selection end to the buffer offset physically above + * the current selection end. + */ + public static boolean extendUp(Spannable text, Layout layout) { + int end = getSelectionEnd(text); + int line = layout.getLineForOffset(end); + + if (line > 0) { + int move; + + if (layout.getParagraphDirection(line) == + layout.getParagraphDirection(line - 1)) { + float h = layout.getPrimaryHorizontal(end); + move = layout.getOffsetForHorizontal(line - 1, h); + } else { + move = layout.getLineStart(line - 1); + } + + extendSelection(text, move); + return true; + } else if (end != 0) { + extendSelection(text, 0); + return true; + } + + return true; + } + + /** + * Move the selection end to the buffer offset physically below + * the current selection end. + */ + public static boolean extendDown(Spannable text, Layout layout) { + int end = getSelectionEnd(text); + int line = layout.getLineForOffset(end); + + if (line < layout.getLineCount() - 1) { + int move; + + if (layout.getParagraphDirection(line) == + layout.getParagraphDirection(line + 1)) { + float h = layout.getPrimaryHorizontal(end); + move = layout.getOffsetForHorizontal(line + 1, h); + } else { + move = layout.getLineStart(line + 1); + } + + extendSelection(text, move); + return true; + } else if (end != text.length()) { + extendSelection(text, text.length()); + return true; + } + + return true; + } + + /** + * Move the selection end to the buffer offset physically to the left of + * the current selection end. + */ + public static boolean extendLeft(Spannable text, Layout layout) { + int end = getSelectionEnd(text); + int to = layout.getOffsetToLeftOf(end); + + if (to != end) { + extendSelection(text, to); + return true; + } + + return true; + } + + /** + * Move the selection end to the buffer offset physically to the right of + * the current selection end. + */ + public static boolean extendRight(Spannable text, Layout layout) { + int end = getSelectionEnd(text); + int to = layout.getOffsetToRightOf(end); + + if (to != end) { + extendSelection(text, to); + return true; + } + + return true; + } + + public static boolean extendToLeftEdge(Spannable text, Layout layout) { + int where = findEdge(text, layout, -1); + extendSelection(text, where); + return true; + } + + public static boolean extendToRightEdge(Spannable text, Layout layout) { + int where = findEdge(text, layout, 1); + extendSelection(text, where); + return true; + } + + public static boolean moveToLeftEdge(Spannable text, Layout layout) { + int where = findEdge(text, layout, -1); + setSelection(text, where); + return true; + } + + public static boolean moveToRightEdge(Spannable text, Layout layout) { + int where = findEdge(text, layout, 1); + setSelection(text, where); + return true; + } + + private static int findEdge(Spannable text, Layout layout, int dir) { + int pt = getSelectionEnd(text); + int line = layout.getLineForOffset(pt); + int pdir = layout.getParagraphDirection(line); + + if (dir * pdir < 0) { + return layout.getLineStart(line); + } else { + int end = layout.getLineEnd(line); + + if (line == layout.getLineCount() - 1) + return end; + else + return end - 1; + } + } + + private static int chooseHorizontal(Layout layout, int direction, + int off1, int off2) { + int line1 = layout.getLineForOffset(off1); + int line2 = layout.getLineForOffset(off2); + + if (line1 == line2) { + // same line, so it goes by pure physical direction + + float h1 = layout.getPrimaryHorizontal(off1); + float h2 = layout.getPrimaryHorizontal(off2); + + if (direction < 0) { + // to left + + if (h1 < h2) + return off1; + else + return off2; + } else { + // to right + + if (h1 > h2) + return off1; + else + return off2; + } + } else { + // different line, so which line is "left" and which is "right" + // depends upon the directionality of the text + + // This only checks at one end, but it's not clear what the + // right thing to do is if the ends don't agree. Even if it + // is wrong it should still not be too bad. + int line = layout.getLineForOffset(off1); + int textdir = layout.getParagraphDirection(line); + + if (textdir == direction) + return Math.max(off1, off2); + else + return Math.min(off1, off2); + } + } + + /* + * Public constants + */ + + public static final Object SELECTION_START = new Object(); + public static final Object SELECTION_END = new Object(); +} diff --git a/core/java/android/text/SpanWatcher.java b/core/java/android/text/SpanWatcher.java new file mode 100644 index 0000000..f99882a --- /dev/null +++ b/core/java/android/text/SpanWatcher.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2006 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; + +/** + * When an object of this type is attached to a Spannable, its methods + * will be called to notify it that other markup objects have been + * added, changed, or removed. + */ +public interface SpanWatcher { + /** + * This method is called to notify you that the specified object + * has been attached to the specified range of the text. + */ + public void onSpanAdded(Spannable text, Object what, int start, int end); + /** + * This method is called to notify you that the specified object + * has been detached from the specified range of the text. + */ + public void onSpanRemoved(Spannable text, Object what, int start, int end); + /** + * This method is called to notify you that the specified object + * has been relocated from the range <code>ostart…oend</code> + * to the new range <code>nstart…nend</code> of the text. + */ + public void onSpanChanged(Spannable text, Object what, int ostart, int oend, + int nstart, int nend); +} diff --git a/core/java/android/text/Spannable.java b/core/java/android/text/Spannable.java new file mode 100644 index 0000000..ae5d356 --- /dev/null +++ b/core/java/android/text/Spannable.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2006 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; + +/** + * This is the interface for text to which markup objects can be + * attached and detached. Not all Spannable classes have mutable text; + * see {@link Editable} for that. + */ +public interface Spannable +extends Spanned +{ + /** + * Attach the specified markup object to the range <code>start…end</code> + * of the text, or move the object to that range if it was already + * attached elsewhere. See {@link Spanned} for an explanation of + * what the flags mean. The object can be one that has meaning only + * within your application, or it can be one that the text system will + * use to affect text display or behavior. Some noteworthy ones are + * the subclasses of {@link android.text.style.CharacterStyle} and + * {@link android.text.style.ParagraphStyle}, and + * {@link android.text.TextWatcher} and + * {@link android.text.SpanWatcher}. + */ + public void setSpan(Object what, int start, int end, int flags); + + /** + * Remove the specified object from the range of text to which it + * was attached, if any. It is OK to remove an object that was never + * attached in the first place. + */ + public void removeSpan(Object what); + + /** + * Factory used by TextView to create new Spannables. You can subclass + * it to provide something other than SpannableString. + */ + public static class Factory { + private static Spannable.Factory sInstance = new Spannable.Factory(); + + /** + * Returns the standard Spannable Factory. + */ + public static Spannable.Factory getInstance() { + return sInstance; + } + + /** + * Returns a new SpannableString from the specified CharSequence. + * You can override this to provide a different kind of Spannable. + */ + public Spannable newSpannable(CharSequence source) { + return new SpannableString(source); + } + } +} diff --git a/core/java/android/text/SpannableString.java b/core/java/android/text/SpannableString.java new file mode 100644 index 0000000..56d0946 --- /dev/null +++ b/core/java/android/text/SpannableString.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2006 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; + + +/** + * This is the class for text whose content is immutable but to which + * markup objects can be attached and detached. + * For mutable text, see {@link SpannableStringBuilder}. + */ +public class SpannableString +extends SpannableStringInternal +implements CharSequence, GetChars, Spannable +{ + public SpannableString(CharSequence source) { + super(source, 0, source.length()); + } + + private SpannableString(CharSequence source, int start, int end) { + super(source, start, end); + } + + public static SpannableString valueOf(CharSequence source) { + if (source instanceof SpannableString) { + return (SpannableString) source; + } else { + return new SpannableString(source); + } + } + + public void setSpan(Object what, int start, int end, int flags) { + super.setSpan(what, start, end, flags); + } + + public void removeSpan(Object what) { + super.removeSpan(what); + } + + public final CharSequence subSequence(int start, int end) { + return new SpannableString(this, start, end); + } +} diff --git a/core/java/android/text/SpannableStringBuilder.java b/core/java/android/text/SpannableStringBuilder.java new file mode 100644 index 0000000..223ce2f --- /dev/null +++ b/core/java/android/text/SpannableStringBuilder.java @@ -0,0 +1,1136 @@ +/* + * Copyright (C) 2006 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; + +import com.android.internal.util.ArrayUtils; +import android.graphics.Paint; +import android.graphics.Canvas; + +import java.lang.reflect.Array; + +/** + * This is the class for text whose content and markup can both be changed. + */ +public class SpannableStringBuilder +implements CharSequence, GetChars, Spannable, Editable, Appendable, + GraphicsOperations +{ + /** + * Create a new SpannableStringBuilder with empty contents + */ + public SpannableStringBuilder() { + this(""); + } + + /** + * Create a new SpannableStringBuilder containing a copy of the + * specified text, including its spans if any. + */ + public SpannableStringBuilder(CharSequence text) { + this(text, 0, text.length()); + } + + /** + * Create a new SpannableStringBuilder containing a copy of the + * specified slice of the specified text, including its spans if any. + */ + public SpannableStringBuilder(CharSequence text, int start, int end) { + int srclen = end - start; + + int len = ArrayUtils.idealCharArraySize(srclen + 1); + mText = new char[len]; + mGapStart = srclen; + mGapLength = len - srclen; + + TextUtils.getChars(text, start, end, mText, 0); + + mSpanCount = 0; + int alloc = ArrayUtils.idealIntArraySize(0); + mSpans = new Object[alloc]; + mSpanStarts = new int[alloc]; + mSpanEnds = new int[alloc]; + mSpanFlags = new int[alloc]; + + if (text instanceof Spanned) { + Spanned sp = (Spanned) text; + Object[] spans = sp.getSpans(start, end, Object.class); + + for (int i = 0; i < spans.length; i++) { + int st = sp.getSpanStart(spans[i]) - start; + int en = sp.getSpanEnd(spans[i]) - start; + int fl = sp.getSpanFlags(spans[i]); + + if (st < 0) + st = 0; + if (st > end - start) + st = end - start; + + if (en < 0) + en = 0; + if (en > end - start) + en = end - start; + + setSpan(spans[i], st, en, fl); + } + } + } + + public static SpannableStringBuilder valueOf(CharSequence source) { + if (source instanceof SpannableStringBuilder) { + return (SpannableStringBuilder) source; + } else { + return new SpannableStringBuilder(source); + } + } + + /** + * Return the char at the specified offset within the buffer. + */ + public char charAt(int where) { + int len = length(); + if (where < 0) { + throw new IndexOutOfBoundsException("charAt: " + where + " < 0"); + } else if (where >= len) { + throw new IndexOutOfBoundsException("charAt: " + where + + " >= length " + len); + } + + if (where >= mGapStart) + return mText[where + mGapLength]; + else + return mText[where]; + } + + /** + * Return the number of chars in the buffer. + */ + public int length() { + return mText.length - mGapLength; + } + + private void resizeFor(int size) { + int newlen = ArrayUtils.idealCharArraySize(size + 1); + char[] newtext = new char[newlen]; + + int after = mText.length - (mGapStart + mGapLength); + + System.arraycopy(mText, 0, newtext, 0, mGapStart); + System.arraycopy(mText, mText.length - after, + newtext, newlen - after, after); + + for (int i = 0; i < mSpanCount; i++) { + if (mSpanStarts[i] > mGapStart) + mSpanStarts[i] += newlen - mText.length; + if (mSpanEnds[i] > mGapStart) + mSpanEnds[i] += newlen - mText.length; + } + + int oldlen = mText.length; + mText = newtext; + mGapLength += mText.length - oldlen; + + if (mGapLength < 1) + new Exception("mGapLength < 1").printStackTrace(); + } + + private void moveGapTo(int where) { + if (where == mGapStart) + return; + + boolean atend = (where == length()); + + if (where < mGapStart) { + int overlap = mGapStart - where; + + System.arraycopy(mText, where, + mText, mGapStart + mGapLength - overlap, overlap); + } else /* where > mGapStart */ { + int overlap = where - mGapStart; + + System.arraycopy(mText, where + mGapLength - overlap, + mText, mGapStart, overlap); + } + + // XXX be more clever + for (int i = 0; i < mSpanCount; i++) { + int start = mSpanStarts[i]; + int end = mSpanEnds[i]; + + if (start > mGapStart) + start -= mGapLength; + if (start > where) + start += mGapLength; + else if (start == where) { + int flag = (mSpanFlags[i] & START_MASK) >> START_SHIFT; + + if (flag == POINT || (atend && flag == PARAGRAPH)) + start += mGapLength; + } + + if (end > mGapStart) + end -= mGapLength; + if (end > where) + end += mGapLength; + else if (end == where) { + int flag = (mSpanFlags[i] & END_MASK); + + if (flag == POINT || (atend && flag == PARAGRAPH)) + end += mGapLength; + } + + mSpanStarts[i] = start; + mSpanEnds[i] = end; + } + + mGapStart = where; + } + + // Documentation from interface + public SpannableStringBuilder insert(int where, CharSequence tb, int start, int end) { + return replace(where, where, tb, start, end); + } + + // Documentation from interface + public SpannableStringBuilder insert(int where, CharSequence tb) { + return replace(where, where, tb, 0, tb.length()); + } + + // Documentation from interface + public SpannableStringBuilder delete(int start, int end) { + SpannableStringBuilder ret = replace(start, end, "", 0, 0); + + if (mGapLength > 2 * length()) + resizeFor(length()); + + return ret; // == this + } + + // Documentation from interface + public void clear() { + replace(0, length(), "", 0, 0); + } + + // Documentation from interface + public void clearSpans() { + for (int i = mSpanCount - 1; i >= 0; i--) { + Object what = mSpans[i]; + int ostart = mSpanStarts[i]; + int oend = mSpanEnds[i]; + + if (ostart > mGapStart) + ostart -= mGapLength; + if (oend > mGapStart) + oend -= mGapLength; + + mSpanCount = i; + mSpans[i] = null; + + sendSpanRemoved(what, ostart, oend); + } + } + + // Documentation from interface + public SpannableStringBuilder append(CharSequence text) { + int length = length(); + return replace(length, length, text, 0, text.length()); + } + + // Documentation from interface + public SpannableStringBuilder append(CharSequence text, int start, int end) { + int length = length(); + return replace(length, length, text, start, end); + } + + // Documentation from interface + public SpannableStringBuilder append(char text) { + return append(String.valueOf(text)); + } + + private int change(int start, int end, + CharSequence tb, int tbstart, int tbend) { + return change(true, start, end, tb, tbstart, tbend); + } + + private int change(boolean notify, int start, int end, + CharSequence tb, int tbstart, int tbend) { + checkRange("replace", start, end); + int ret = tbend - tbstart; + TextWatcher[] recipients = null; + + if (notify) + recipients = sendTextWillChange(start, end - start, + tbend - tbstart); + + for (int i = mSpanCount - 1; i >= 0; i--) { + if ((mSpanFlags[i] & SPAN_PARAGRAPH) == SPAN_PARAGRAPH) { + int st = mSpanStarts[i]; + if (st > mGapStart) + st -= mGapLength; + + int en = mSpanEnds[i]; + if (en > mGapStart) + en -= mGapLength; + + int ost = st; + int oen = en; + int clen = length(); + + if (st > start && st <= end) { + for (st = end; st < clen; st++) + if (st > end && charAt(st - 1) == '\n') + break; + } + + if (en > start && en <= end) { + for (en = end; en < clen; en++) + if (en > end && charAt(en - 1) == '\n') + break; + } + + if (st != ost || en != oen) + setSpan(mSpans[i], st, en, mSpanFlags[i]); + } + } + + moveGapTo(end); + + if (tbend - tbstart >= mGapLength + (end - start)) + resizeFor(mText.length - mGapLength + + tbend - tbstart - (end - start)); + + mGapStart += tbend - tbstart - (end - start); + mGapLength -= tbend - tbstart - (end - start); + + if (mGapLength < 1) + new Exception("mGapLength < 1").printStackTrace(); + + TextUtils.getChars(tb, tbstart, tbend, mText, start); + + if (tb instanceof Spanned) { + Spanned sp = (Spanned) tb; + Object[] spans = sp.getSpans(tbstart, tbend, Object.class); + + for (int i = 0; i < spans.length; i++) { + int st = sp.getSpanStart(spans[i]); + int en = sp.getSpanEnd(spans[i]); + + if (st < tbstart) + st = tbstart; + if (en > tbend) + en = tbend; + + if (getSpanStart(spans[i]) < 0) { + setSpan(false, spans[i], + st - tbstart + start, + en - tbstart + start, + sp.getSpanFlags(spans[i])); + } + } + } + + // no need for span fixup on pure insertion + if (tbend > tbstart && end - start == 0) { + if (notify) { + sendTextChange(recipients, start, end - start, tbend - tbstart); + sendTextHasChanged(recipients); + } + + return ret; + } + + boolean atend = (mGapStart + mGapLength == mText.length); + + for (int i = mSpanCount - 1; i >= 0; i--) { + if (mSpanStarts[i] >= start && + mSpanStarts[i] < mGapStart + mGapLength) { + int flag = (mSpanFlags[i] & START_MASK) >> START_SHIFT; + + if (flag == POINT || (flag == PARAGRAPH && atend)) + mSpanStarts[i] = mGapStart + mGapLength; + else + mSpanStarts[i] = start; + } + + if (mSpanEnds[i] >= start && + mSpanEnds[i] < mGapStart + mGapLength) { + int flag = (mSpanFlags[i] & END_MASK); + + if (flag == POINT || (flag == PARAGRAPH && atend)) + mSpanEnds[i] = mGapStart + mGapLength; + else + mSpanEnds[i] = start; + } + + // remove 0-length SPAN_EXCLUSIVE_EXCLUSIVE + // XXX send notification on removal + + if (mSpanEnds[i] < mSpanStarts[i]) { + System.arraycopy(mSpans, i + 1, + mSpans, i, mSpanCount - (i + 1)); + System.arraycopy(mSpanStarts, i + 1, + mSpanStarts, i, mSpanCount - (i + 1)); + System.arraycopy(mSpanEnds, i + 1, + mSpanEnds, i, mSpanCount - (i + 1)); + System.arraycopy(mSpanFlags, i + 1, + mSpanFlags, i, mSpanCount - (i + 1)); + + mSpanCount--; + } + } + + if (notify) { + sendTextChange(recipients, start, end - start, tbend - tbstart); + sendTextHasChanged(recipients); + } + + return ret; + } + + // Documentation from interface + public SpannableStringBuilder replace(int start, int end, CharSequence tb) { + return replace(start, end, tb, 0, tb.length()); + } + + // Documentation from interface + public SpannableStringBuilder replace(final int start, final int end, + CharSequence tb, int tbstart, int tbend) { + int filtercount = mFilters.length; + for (int i = 0; i < filtercount; i++) { + CharSequence repl = mFilters[i].filter(tb, tbstart, tbend, + this, start, end); + + if (repl != null) { + tb = repl; + tbstart = 0; + tbend = repl.length(); + } + } + + if (end == start && tbstart == tbend) { + return this; + } + + if (end == start || tbstart == tbend) { + change(start, end, tb, tbstart, tbend); + } else { + int selstart = Selection.getSelectionStart(this); + int selend = Selection.getSelectionEnd(this); + + // XXX just make the span fixups in change() do the right thing + // instead of this madness! + + checkRange("replace", start, end); + moveGapTo(end); + TextWatcher[] recipients; + + recipients = sendTextWillChange(start, end - start, + tbend - tbstart); + + int origlen = end - start; + + if (mGapLength < 2) + resizeFor(length() + 1); + + for (int i = mSpanCount - 1; i >= 0; i--) { + if (mSpanStarts[i] == mGapStart) + mSpanStarts[i]++; + + if (mSpanEnds[i] == mGapStart) + mSpanEnds[i]++; + } + + mText[mGapStart] = ' '; + mGapStart++; + mGapLength--; + + if (mGapLength < 1) + new Exception("mGapLength < 1").printStackTrace(); + + int oldlen = (end + 1) - start; + + int inserted = change(false, start + 1, start + 1, + tb, tbstart, tbend); + change(false, start, start + 1, "", 0, 0); + change(false, start + inserted, start + inserted + oldlen - 1, + "", 0, 0); + + /* + * Special case to keep the cursor in the same position + * if it was somewhere in the middle of the replaced region. + * If it was at the start or the end or crossing the whole + * replacement, it should already be where it belongs. + * TODO: Is there some more general mechanism that could + * accomplish this? + */ + if (selstart > start && selstart < end) { + long off = selstart - start; + + off = off * inserted / (end - start); + selstart = (int) off + start; + + setSpan(false, Selection.SELECTION_START, selstart, selstart, + Spanned.SPAN_POINT_POINT); + } + if (selend > start && selend < end) { + long off = selend - start; + + off = off * inserted / (end - start); + selend = (int) off + start; + + setSpan(false, Selection.SELECTION_END, selend, selend, + Spanned.SPAN_POINT_POINT); + } + + sendTextChange(recipients, start, origlen, inserted); + sendTextHasChanged(recipients); + } + return this; + } + + /** + * Mark the specified range of text with the specified object. + * The flags determine how the span will behave when text is + * inserted at the start or end of the span's range. + */ + public void setSpan(Object what, int start, int end, int flags) { + setSpan(true, what, start, end, flags); + } + + private void setSpan(boolean send, + Object what, int start, int end, int flags) { + int nstart = start; + int nend = end; + + checkRange("setSpan", start, end); + + if ((flags & START_MASK) == (PARAGRAPH << START_SHIFT)) { + if (start != 0 && start != length()) { + char c = charAt(start - 1); + + if (c != '\n') + throw new RuntimeException( + "PARAGRAPH span must start at paragraph boundary"); + } + } + + if ((flags & END_MASK) == PARAGRAPH) { + if (end != 0 && end != length()) { + char c = charAt(end - 1); + + if (c != '\n') + throw new RuntimeException( + "PARAGRAPH span must end at paragraph boundary"); + } + } + + if (start > mGapStart) + start += mGapLength; + else if (start == mGapStart) { + int flag = (flags & START_MASK) >> START_SHIFT; + + if (flag == POINT || (flag == PARAGRAPH && start == length())) + start += mGapLength; + } + + if (end > mGapStart) + end += mGapLength; + else if (end == mGapStart) { + int flag = (flags & END_MASK); + + if (flag == POINT || (flag == PARAGRAPH && end == length())) + end += mGapLength; + } + + int count = mSpanCount; + Object[] spans = mSpans; + + for (int i = 0; i < count; i++) { + if (spans[i] == what) { + int ostart = mSpanStarts[i]; + int oend = mSpanEnds[i]; + + if (ostart > mGapStart) + ostart -= mGapLength; + if (oend > mGapStart) + oend -= mGapLength; + + mSpanStarts[i] = start; + mSpanEnds[i] = end; + mSpanFlags[i] = flags; + + if (send) + sendSpanChanged(what, ostart, oend, nstart, nend); + + return; + } + } + + if (mSpanCount + 1 >= mSpans.length) { + int newsize = ArrayUtils.idealIntArraySize(mSpanCount + 1); + Object[] newspans = new Object[newsize]; + int[] newspanstarts = new int[newsize]; + int[] newspanends = new int[newsize]; + int[] newspanflags = new int[newsize]; + + System.arraycopy(mSpans, 0, newspans, 0, mSpanCount); + System.arraycopy(mSpanStarts, 0, newspanstarts, 0, mSpanCount); + System.arraycopy(mSpanEnds, 0, newspanends, 0, mSpanCount); + System.arraycopy(mSpanFlags, 0, newspanflags, 0, mSpanCount); + + mSpans = newspans; + mSpanStarts = newspanstarts; + mSpanEnds = newspanends; + mSpanFlags = newspanflags; + } + + mSpans[mSpanCount] = what; + mSpanStarts[mSpanCount] = start; + mSpanEnds[mSpanCount] = end; + mSpanFlags[mSpanCount] = flags; + mSpanCount++; + + if (send) + sendSpanAdded(what, nstart, nend); + } + + /** + * Remove the specified markup object from the buffer. + */ + public void removeSpan(Object what) { + for (int i = mSpanCount - 1; i >= 0; i--) { + if (mSpans[i] == what) { + int ostart = mSpanStarts[i]; + int oend = mSpanEnds[i]; + + if (ostart > mGapStart) + ostart -= mGapLength; + if (oend > mGapStart) + oend -= mGapLength; + + int count = mSpanCount - (i + 1); + + System.arraycopy(mSpans, i + 1, mSpans, i, count); + System.arraycopy(mSpanStarts, i + 1, mSpanStarts, i, count); + System.arraycopy(mSpanEnds, i + 1, mSpanEnds, i, count); + System.arraycopy(mSpanFlags, i + 1, mSpanFlags, i, count); + + mSpanCount--; + mSpans[mSpanCount] = null; + + sendSpanRemoved(what, ostart, oend); + return; + } + } + } + + /** + * Return the buffer offset of the beginning of the specified + * markup object, or -1 if it is not attached to this buffer. + */ + public int getSpanStart(Object what) { + int count = mSpanCount; + Object[] spans = mSpans; + + for (int i = count - 1; i >= 0; i--) { + if (spans[i] == what) { + int where = mSpanStarts[i]; + + if (where > mGapStart) + where -= mGapLength; + + return where; + } + } + + return -1; + } + + /** + * Return the buffer offset of the end of the specified + * markup object, or -1 if it is not attached to this buffer. + */ + public int getSpanEnd(Object what) { + int count = mSpanCount; + Object[] spans = mSpans; + + for (int i = count - 1; i >= 0; i--) { + if (spans[i] == what) { + int where = mSpanEnds[i]; + + if (where > mGapStart) + where -= mGapLength; + + return where; + } + } + + return -1; + } + + /** + * Return the flags of the end of the specified + * markup object, or 0 if it is not attached to this buffer. + */ + public int getSpanFlags(Object what) { + int count = mSpanCount; + Object[] spans = mSpans; + + for (int i = count - 1; i >= 0; i--) { + if (spans[i] == what) { + return mSpanFlags[i]; + } + } + + return 0; + } + + /** + * Return an array of the spans of the specified type that overlap + * the specified range of the buffer. The kind may be Object.class to get + * a list of all the spans regardless of type. + */ + public <T> T[] getSpans(int queryStart, int queryEnd, Class<T> kind) { + int spanCount = mSpanCount; + Object[] spans = mSpans; + int[] starts = mSpanStarts; + int[] ends = mSpanEnds; + int[] flags = mSpanFlags; + int gapstart = mGapStart; + int gaplen = mGapLength; + + int count = 0; + Object[] ret = null; + Object ret1 = null; + + for (int i = 0; i < spanCount; i++) { + int spanStart = starts[i]; + int spanEnd = ends[i]; + + if (spanStart > gapstart) { + spanStart -= gaplen; + } + if (spanEnd > gapstart) { + spanEnd -= gaplen; + } + + if (spanStart > queryEnd) { + continue; + } + if (spanEnd < queryStart) { + continue; + } + + if (spanStart != spanEnd && queryStart != queryEnd) { + if (spanStart == queryEnd) + continue; + if (spanEnd == queryStart) + continue; + } + + if (kind != null && !kind.isInstance(spans[i])) { + continue; + } + + if (count == 0) { + ret1 = spans[i]; + count++; + } else { + if (count == 1) { + ret = (Object[]) Array.newInstance(kind, spanCount - i + 1); + ret[0] = ret1; + } + + int prio = flags[i] & SPAN_PRIORITY; + if (prio != 0) { + int j; + + for (j = 0; j < count; j++) { + int p = getSpanFlags(ret[j]) & SPAN_PRIORITY; + + if (prio > p) { + break; + } + } + + System.arraycopy(ret, j, ret, j + 1, count - j); + ret[j] = spans[i]; + count++; + } else { + ret[count++] = spans[i]; + } + } + } + + if (count == 0) { + return (T[]) ArrayUtils.emptyArray(kind); + } + if (count == 1) { + ret = (Object[]) Array.newInstance(kind, 1); + ret[0] = ret1; + return (T[]) ret; + } + if (count == ret.length) { + return (T[]) ret; + } + + Object[] nret = (Object[]) Array.newInstance(kind, count); + System.arraycopy(ret, 0, nret, 0, count); + return (T[]) nret; + } + + /** + * Return the next offset after <code>start</code> but less than or + * equal to <code>limit</code> where a span of the specified type + * begins or ends. + */ + public int nextSpanTransition(int start, int limit, Class kind) { + int count = mSpanCount; + Object[] spans = mSpans; + int[] starts = mSpanStarts; + int[] ends = mSpanEnds; + int gapstart = mGapStart; + int gaplen = mGapLength; + + if (kind == null) { + kind = Object.class; + } + + for (int i = 0; i < count; i++) { + int st = starts[i]; + int en = ends[i]; + + if (st > gapstart) + st -= gaplen; + if (en > gapstart) + en -= gaplen; + + if (st > start && st < limit && kind.isInstance(spans[i])) + limit = st; + if (en > start && en < limit && kind.isInstance(spans[i])) + limit = en; + } + + return limit; + } + + /** + * Return a new CharSequence containing a copy of the specified + * range of this buffer, including the overlapping spans. + */ + public CharSequence subSequence(int start, int end) { + return new SpannableStringBuilder(this, start, end); + } + + /** + * Copy the specified range of chars from this buffer into the + * specified array, beginning at the specified offset. + */ + public void getChars(int start, int end, char[] dest, int destoff) { + checkRange("getChars", start, end); + + if (end <= mGapStart) { + System.arraycopy(mText, start, dest, destoff, end - start); + } else if (start >= mGapStart) { + System.arraycopy(mText, start + mGapLength, + dest, destoff, end - start); + } else { + System.arraycopy(mText, start, dest, destoff, mGapStart - start); + System.arraycopy(mText, mGapStart + mGapLength, + dest, destoff + (mGapStart - start), + end - mGapStart); + } + } + + /** + * Return a String containing a copy of the chars in this buffer. + */ + public String toString() { + int len = length(); + char[] buf = new char[len]; + + getChars(0, len, buf, 0); + return new String(buf); + } + + private TextWatcher[] sendTextWillChange(int start, int before, int after) { + TextWatcher[] recip = getSpans(start, start + before, TextWatcher.class); + int n = recip.length; + + for (int i = 0; i < n; i++) { + recip[i].beforeTextChanged(this, start, before, after); + } + + return recip; + } + + private void sendTextChange(TextWatcher[] recip, int start, int before, + int after) { + int n = recip.length; + + for (int i = 0; i < n; i++) { + recip[i].onTextChanged(this, start, before, after); + } + } + + private void sendTextHasChanged(TextWatcher[] recip) { + int n = recip.length; + + for (int i = 0; i < n; i++) { + recip[i].afterTextChanged(this); + } + } + + private void sendSpanAdded(Object what, int start, int end) { + SpanWatcher[] recip = getSpans(start, end, SpanWatcher.class); + int n = recip.length; + + for (int i = 0; i < n; i++) { + recip[i].onSpanAdded(this, what, start, end); + } + } + + private void sendSpanRemoved(Object what, int start, int end) { + SpanWatcher[] recip = getSpans(start, end, SpanWatcher.class); + int n = recip.length; + + for (int i = 0; i < n; i++) { + recip[i].onSpanRemoved(this, what, start, end); + } + } + + private void sendSpanChanged(Object what, int s, int e, int st, int en) { + SpanWatcher[] recip = getSpans(Math.min(s, st), Math.max(e, en), + SpanWatcher.class); + int n = recip.length; + + for (int i = 0; i < n; i++) { + recip[i].onSpanChanged(this, what, s, e, st, en); + } + } + + private static String region(int start, int end) { + return "(" + start + " ... " + end + ")"; + } + + private void checkRange(final String operation, int start, int end) { + if (end < start) { + throw new IndexOutOfBoundsException(operation + " " + + region(start, end) + + " has end before start"); + } + + int len = length(); + + if (start > len || end > len) { + throw new IndexOutOfBoundsException(operation + " " + + region(start, end) + + " ends beyond length " + len); + } + + if (start < 0 || end < 0) { + throw new IndexOutOfBoundsException(operation + " " + + region(start, end) + + " starts before 0"); + } + } + + private boolean isprint(char c) { // XXX + if (c >= ' ' && c <= '~') + return true; + else + return false; + } + +/* + private static final int startFlag(int flag) { + return (flag >> 4) & 0x0F; + } + + private static final int endFlag(int flag) { + return flag & 0x0F; + } + + public void dump() { // XXX + for (int i = 0; i < mGapStart; i++) { + System.out.print('|'); + System.out.print(' '); + System.out.print(isprint(mText[i]) ? mText[i] : '.'); + System.out.print(' '); + } + + for (int i = mGapStart; i < mGapStart + mGapLength; i++) { + System.out.print('|'); + System.out.print('('); + System.out.print(isprint(mText[i]) ? mText[i] : '.'); + System.out.print(')'); + } + + for (int i = mGapStart + mGapLength; i < mText.length; i++) { + System.out.print('|'); + System.out.print(' '); + System.out.print(isprint(mText[i]) ? mText[i] : '.'); + System.out.print(' '); + } + + System.out.print('\n'); + + for (int i = 0; i < mText.length + 1; i++) { + int found = 0; + int wfound = 0; + + for (int j = 0; j < mSpanCount; j++) { + if (mSpanStarts[j] == i) { + found = 1; + wfound = j; + break; + } + + if (mSpanEnds[j] == i) { + found = 2; + wfound = j; + break; + } + } + + if (found == 1) { + if (startFlag(mSpanFlags[wfound]) == MARK) + System.out.print("( "); + if (startFlag(mSpanFlags[wfound]) == PARAGRAPH) + System.out.print("< "); + else + System.out.print("[ "); + } else if (found == 2) { + if (endFlag(mSpanFlags[wfound]) == POINT) + System.out.print(") "); + if (endFlag(mSpanFlags[wfound]) == PARAGRAPH) + System.out.print("> "); + else + System.out.print("] "); + } else { + System.out.print(" "); + } + } + + System.out.print("\n"); + } +*/ + + /** + * Don't call this yourself -- exists for Canvas to use internally. + * {@hide} + */ + public void drawText(Canvas c, int start, int end, + float x, float y, Paint p) { + checkRange("drawText", start, end); + + if (end <= mGapStart) { + c.drawText(mText, start, end - start, x, y, p); + } else if (start >= mGapStart) { + c.drawText(mText, start + mGapLength, end - start, x, y, p); + } else { + char[] buf = TextUtils.obtain(end - start); + + getChars(start, end, buf, 0); + c.drawText(buf, 0, end - start, x, y, p); + TextUtils.recycle(buf); + } + } + + /** + * Don't call this yourself -- exists for Paint to use internally. + * {@hide} + */ + public float measureText(int start, int end, Paint p) { + checkRange("measureText", start, end); + + float ret; + + if (end <= mGapStart) { + ret = p.measureText(mText, start, end - start); + } else if (start >= mGapStart) { + ret = p.measureText(mText, start + mGapLength, end - start); + } else { + char[] buf = TextUtils.obtain(end - start); + + getChars(start, end, buf, 0); + ret = p.measureText(buf, 0, end - start); + TextUtils.recycle(buf); + } + + return ret; + } + + /** + * Don't call this yourself -- exists for Paint to use internally. + * {@hide} + */ + public int getTextWidths(int start, int end, float[] widths, Paint p) { + checkRange("getTextWidths", start, end); + + int ret; + + if (end <= mGapStart) { + ret = p.getTextWidths(mText, start, end - start, widths); + } else if (start >= mGapStart) { + ret = p.getTextWidths(mText, start + mGapLength, end - start, + widths); + } else { + char[] buf = TextUtils.obtain(end - start); + + getChars(start, end, buf, 0); + ret = p.getTextWidths(buf, 0, end - start, widths); + TextUtils.recycle(buf); + } + + return ret; + } + + // Documentation from interface + public void setFilters(InputFilter[] filters) { + if (filters == null) { + throw new IllegalArgumentException(); + } + + mFilters = filters; + } + + // Documentation from interface + public InputFilter[] getFilters() { + return mFilters; + } + + private static final InputFilter[] NO_FILTERS = new InputFilter[0]; + private InputFilter[] mFilters = NO_FILTERS; + + private char[] mText; + private int mGapStart; + private int mGapLength; + + private Object[] mSpans; + private int[] mSpanStarts; + private int[] mSpanEnds; + private int[] mSpanFlags; + private int mSpanCount; + + private static final int MARK = 1; + private static final int POINT = 2; + private static final int PARAGRAPH = 3; + + private static final int START_MASK = 0xF0; + private static final int END_MASK = 0x0F; + private static final int START_SHIFT = 4; +} diff --git a/core/java/android/text/SpannableStringInternal.java b/core/java/android/text/SpannableStringInternal.java new file mode 100644 index 0000000..0412285 --- /dev/null +++ b/core/java/android/text/SpannableStringInternal.java @@ -0,0 +1,372 @@ +/* + * Copyright (C) 2006 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; + +import com.android.internal.util.ArrayUtils; + +import java.lang.reflect.Array; + +/* package */ abstract class SpannableStringInternal +{ + /* package */ SpannableStringInternal(CharSequence source, + int start, int end) { + if (start == 0 && end == source.length()) + mText = source.toString(); + else + mText = source.toString().substring(start, end); + + int initial = ArrayUtils.idealIntArraySize(0); + mSpans = new Object[initial]; + mSpanData = new int[initial * 3]; + + if (source instanceof Spanned) { + Spanned sp = (Spanned) source; + Object[] spans = sp.getSpans(start, end, Object.class); + + for (int i = 0; i < spans.length; i++) { + int st = sp.getSpanStart(spans[i]); + int en = sp.getSpanEnd(spans[i]); + int fl = sp.getSpanFlags(spans[i]); + + if (st < start) + st = start; + if (en > end) + en = end; + + setSpan(spans[i], st - start, en - start, fl); + } + } + } + + public final int length() { + return mText.length(); + } + + public final char charAt(int i) { + return mText.charAt(i); + } + + public final String toString() { + return mText; + } + + /* subclasses must do subSequence() to preserve type */ + + public final void getChars(int start, int end, char[] dest, int off) { + mText.getChars(start, end, dest, off); + } + + /* package */ void setSpan(Object what, int start, int end, int flags) { + int nstart = start; + int nend = end; + + checkRange("setSpan", start, end); + + if ((flags & Spannable.SPAN_PARAGRAPH) == Spannable.SPAN_PARAGRAPH) { + if (start != 0 && start != length()) { + char c = charAt(start - 1); + + if (c != '\n') + throw new RuntimeException( + "PARAGRAPH span must start at paragraph boundary" + + " (" + start + " follows " + c + ")"); + } + + if (end != 0 && end != length()) { + char c = charAt(end - 1); + + if (c != '\n') + throw new RuntimeException( + "PARAGRAPH span must end at paragraph boundary" + + " (" + end + " follows " + c + ")"); + } + } + + int count = mSpanCount; + Object[] spans = mSpans; + int[] data = mSpanData; + + for (int i = 0; i < count; i++) { + if (spans[i] == what) { + int ostart = data[i * COLUMNS + START]; + int oend = data[i * COLUMNS + END]; + + data[i * COLUMNS + START] = start; + data[i * COLUMNS + END] = end; + data[i * COLUMNS + FLAGS] = flags; + + sendSpanChanged(what, ostart, oend, nstart, nend); + return; + } + } + + if (mSpanCount + 1 >= mSpans.length) { + int newsize = ArrayUtils.idealIntArraySize(mSpanCount + 1); + Object[] newtags = new Object[newsize]; + int[] newdata = new int[newsize * 3]; + + System.arraycopy(mSpans, 0, newtags, 0, mSpanCount); + System.arraycopy(mSpanData, 0, newdata, 0, mSpanCount * 3); + + mSpans = newtags; + mSpanData = newdata; + } + + mSpans[mSpanCount] = what; + mSpanData[mSpanCount * COLUMNS + START] = start; + mSpanData[mSpanCount * COLUMNS + END] = end; + mSpanData[mSpanCount * COLUMNS + FLAGS] = flags; + mSpanCount++; + + if (this instanceof Spannable) + sendSpanAdded(what, nstart, nend); + } + + /* package */ void removeSpan(Object what) { + int count = mSpanCount; + Object[] spans = mSpans; + int[] data = mSpanData; + + for (int i = count - 1; i >= 0; i--) { + if (spans[i] == what) { + int ostart = data[i * COLUMNS + START]; + int oend = data[i * COLUMNS + END]; + + int c = count - (i + 1); + + System.arraycopy(spans, i + 1, spans, i, c); + System.arraycopy(data, (i + 1) * COLUMNS, + data, i * COLUMNS, c * COLUMNS); + + mSpanCount--; + + sendSpanRemoved(what, ostart, oend); + return; + } + } + } + + public int getSpanStart(Object what) { + int count = mSpanCount; + Object[] spans = mSpans; + int[] data = mSpanData; + + for (int i = count - 1; i >= 0; i--) { + if (spans[i] == what) { + return data[i * COLUMNS + START]; + } + } + + return -1; + } + + public int getSpanEnd(Object what) { + int count = mSpanCount; + Object[] spans = mSpans; + int[] data = mSpanData; + + for (int i = count - 1; i >= 0; i--) { + if (spans[i] == what) { + return data[i * COLUMNS + END]; + } + } + + return -1; + } + + public int getSpanFlags(Object what) { + int count = mSpanCount; + Object[] spans = mSpans; + int[] data = mSpanData; + + for (int i = count - 1; i >= 0; i--) { + if (spans[i] == what) { + return data[i * COLUMNS + FLAGS]; + } + } + + return 0; + } + + public <T> T[] getSpans(int queryStart, int queryEnd, Class<T> kind) { + int count = 0; + + int spanCount = mSpanCount; + Object[] spans = mSpans; + int[] data = mSpanData; + Object[] ret = null; + Object ret1 = null; + + for (int i = 0; i < spanCount; i++) { + int spanStart = data[i * COLUMNS + START]; + int spanEnd = data[i * COLUMNS + END]; + + if (spanStart > queryEnd) { + continue; + } + if (spanEnd < queryStart) { + continue; + } + + if (spanStart != spanEnd && queryStart != queryEnd) { + if (spanStart == queryEnd) { + continue; + } + if (spanEnd == queryStart) { + continue; + } + } + + if (kind != null && !kind.isInstance(spans[i])) { + continue; + } + + if (count == 0) { + ret1 = spans[i]; + count++; + } else { + if (count == 1) { + ret = (Object[]) Array.newInstance(kind, spanCount - i + 1); + ret[0] = ret1; + } + + int prio = data[i * COLUMNS + FLAGS] & Spanned.SPAN_PRIORITY; + if (prio != 0) { + int j; + + for (j = 0; j < count; j++) { + int p = getSpanFlags(ret[j]) & Spanned.SPAN_PRIORITY; + + if (prio > p) { + break; + } + } + + System.arraycopy(ret, j, ret, j + 1, count - j); + ret[j] = spans[i]; + count++; + } else { + ret[count++] = spans[i]; + } + } + } + + if (count == 0) { + return (T[]) ArrayUtils.emptyArray(kind); + } + if (count == 1) { + ret = (Object[]) Array.newInstance(kind, 1); + ret[0] = ret1; + return (T[]) ret; + } + if (count == ret.length) { + return (T[]) ret; + } + + Object[] nret = (Object[]) Array.newInstance(kind, count); + System.arraycopy(ret, 0, nret, 0, count); + return (T[]) nret; + } + + public int nextSpanTransition(int start, int limit, Class kind) { + int count = mSpanCount; + Object[] spans = mSpans; + int[] data = mSpanData; + + if (kind == null) { + kind = Object.class; + } + + for (int i = 0; i < count; i++) { + int st = data[i * COLUMNS + START]; + int en = data[i * COLUMNS + END]; + + if (st > start && st < limit && kind.isInstance(spans[i])) + limit = st; + if (en > start && en < limit && kind.isInstance(spans[i])) + limit = en; + } + + return limit; + } + + private void sendSpanAdded(Object what, int start, int end) { + SpanWatcher[] recip = getSpans(start, end, SpanWatcher.class); + int n = recip.length; + + for (int i = 0; i < n; i++) { + recip[i].onSpanAdded((Spannable) this, what, start, end); + } + } + + private void sendSpanRemoved(Object what, int start, int end) { + SpanWatcher[] recip = getSpans(start, end, SpanWatcher.class); + int n = recip.length; + + for (int i = 0; i < n; i++) { + recip[i].onSpanRemoved((Spannable) this, what, start, end); + } + } + + private void sendSpanChanged(Object what, int s, int e, int st, int en) { + SpanWatcher[] recip = getSpans(Math.min(s, st), Math.max(e, en), + SpanWatcher.class); + int n = recip.length; + + for (int i = 0; i < n; i++) { + recip[i].onSpanChanged((Spannable) this, what, s, e, st, en); + } + } + + private static String region(int start, int end) { + return "(" + start + " ... " + end + ")"; + } + + private void checkRange(final String operation, int start, int end) { + if (end < start) { + throw new IndexOutOfBoundsException(operation + " " + + region(start, end) + + " has end before start"); + } + + int len = length(); + + if (start > len || end > len) { + throw new IndexOutOfBoundsException(operation + " " + + region(start, end) + + " ends beyond length " + len); + } + + if (start < 0 || end < 0) { + throw new IndexOutOfBoundsException(operation + " " + + region(start, end) + + " starts before 0"); + } + } + + private String mText; + private Object[] mSpans; + private int[] mSpanData; + private int mSpanCount; + + /* package */ static final Object[] EMPTY = new Object[0]; + + private static final int START = 0; + private static final int END = 1; + private static final int FLAGS = 2; + private static final int COLUMNS = 3; +} diff --git a/core/java/android/text/Spanned.java b/core/java/android/text/Spanned.java new file mode 100644 index 0000000..2b4b4d2 --- /dev/null +++ b/core/java/android/text/Spanned.java @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2006 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; + +/** + * This is the interface for text that has markup objects attached to + * ranges of it. Not all text classes have mutable markup or text; + * see {@link Spannable} for mutable markup and {@link Editable} for + * mutable text. + */ +public interface Spanned +extends CharSequence +{ + /** + * 0-length spans with type SPAN_MARK_MARK behave like text marks: + * they remain at their original offset when text is inserted + * at that offset. + */ + public static final int SPAN_MARK_MARK = 0x11; + /** + * SPAN_MARK_POINT is a synonym for {@link #SPAN_INCLUSIVE_INCLUSIVE}. + */ + public static final int SPAN_MARK_POINT = 0x12; + /** + * SPAN_POINT_MARK is a synonym for {@link #SPAN_EXCLUSIVE_EXCLUSIVE}. + */ + public static final int SPAN_POINT_MARK = 0x21; + + /** + * 0-length spans with type SPAN_POINT_POINT behave like cursors: + * they are pushed forward by the length of the insertion when text + * is inserted at their offset. + */ + public static final int SPAN_POINT_POINT = 0x22; + + /** + * SPAN_PARAGRAPH behaves like SPAN_INCLUSIVE_EXCLUSIVE + * (SPAN_MARK_MARK), except that if either end of the span is + * at the end of the buffer, that end behaves like _POINT + * instead (so SPAN_INCLUSIVE_INCLUSIVE if it starts in the + * middle and ends at the end, or SPAN_EXCLUSIVE_INCLUSIVE + * if it both starts and ends at the end). + * <p> + * Its endpoints must be the start or end of the buffer or + * immediately after a \n character, and if the \n + * that anchors it is deleted, the endpoint is pulled to the + * next \n that follows in the buffer (or to the end of + * the buffer). + */ + public static final int SPAN_PARAGRAPH = 0x33; + + /** + * Non-0-length spans of type SPAN_INCLUSIVE_EXCLUSIVE expand + * to include text inserted at their starting point but not at their + * ending point. When 0-length, they behave like marks. + */ + public static final int SPAN_INCLUSIVE_EXCLUSIVE = SPAN_MARK_MARK; + + /** + * Spans of type SPAN_INCLUSIVE_INCLUSIVE expand + * to include text inserted at either their starting or ending point. + */ + public static final int SPAN_INCLUSIVE_INCLUSIVE = SPAN_MARK_POINT; + + /** + * Spans of type SPAN_EXCLUSIVE_EXCLUSIVE do not expand + * to include text inserted at either their starting or ending point. + * They can never have a length of 0 and are automatically removed + * from the buffer if all the text they cover is removed. + */ + public static final int SPAN_EXCLUSIVE_EXCLUSIVE = SPAN_POINT_MARK; + + /** + * Non-0-length spans of type SPAN_INCLUSIVE_EXCLUSIVE expand + * to include text inserted at their ending point but not at their + * starting point. When 0-length, they behave like points. + */ + public static final int SPAN_EXCLUSIVE_INCLUSIVE = SPAN_POINT_POINT; + + /** + * The bits numbered SPAN_USER_SHIFT and above are available + * for callers to use to store scalar data associated with their + * span object. + */ + public static final int SPAN_USER_SHIFT = 24; + /** + * The bits specified by the SPAN_USER bitfield are available + * for callers to use to store scalar data associated with their + * span object. + */ + public static final int SPAN_USER = 0xFFFFFFFF << SPAN_USER_SHIFT; + + /** + * The bits numbered just above SPAN_PRIORITY_SHIFT determine the order + * of change notifications -- higher numbers go first. You probably + * don't need to set this; it is used so that when text changes, the + * text layout gets the chance to update itself before any other + * callbacks can inquire about the layout of the text. + */ + public static final int SPAN_PRIORITY_SHIFT = 16; + /** + * The bits specified by the SPAN_PRIORITY bitmap determine the order + * of change notifications -- higher numbers go first. You probably + * don't need to set this; it is used so that when text changes, the + * text layout gets the chance to update itself before any other + * callbacks can inquire about the layout of the text. + */ + public static final int SPAN_PRIORITY = 0xFF << SPAN_PRIORITY_SHIFT; + + /** + * Return an array of the markup objects attached to the specified + * slice of this CharSequence and whose type is the specified type + * or a subclass of it. Specify Object.class for the type if you + * want all the objects regardless of type. + */ + public <T> T[] getSpans(int start, int end, Class<T> type); + + /** + * Return the beginning of the range of text to which the specified + * markup object is attached, or -1 if the object is not attached. + */ + public int getSpanStart(Object tag); + + /** + * Return the end of the range of text to which the specified + * markup object is attached, or -1 if the object is not attached. + */ + public int getSpanEnd(Object tag); + + /** + * Return the flags that were specified when {@link Spannable#setSpan} was + * used to attach the specified markup object, or 0 if the specified + * object has not been attached. + */ + public int getSpanFlags(Object tag); + + /** + * Return the first offset greater than or equal to <code>start</code> + * where a markup object of class <code>type</code> begins or ends, + * or <code>limit</code> if there are no starts or ends greater than or + * equal to <code>start</code> but less than <code>limit</code>. Specify + * <code>null</code> or Object.class for the type if you want every + * transition regardless of type. + */ + public int nextSpanTransition(int start, int limit, Class type); +} diff --git a/core/java/android/text/SpannedString.java b/core/java/android/text/SpannedString.java new file mode 100644 index 0000000..afed221 --- /dev/null +++ b/core/java/android/text/SpannedString.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2006 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; + + +/** + * This is the class for text whose content and markup are immutable. + * For mutable markup, see {@link SpannableString}; for mutable text, + * see {@link SpannableStringBuilder}. + */ +public final class SpannedString +extends SpannableStringInternal +implements CharSequence, GetChars, Spanned +{ + public SpannedString(CharSequence source) { + super(source, 0, source.length()); + } + + private SpannedString(CharSequence source, int start, int end) { + super(source, start, end); + } + + public CharSequence subSequence(int start, int end) { + return new SpannedString(this, start, end); + } + + public static SpannedString valueOf(CharSequence source) { + if (source instanceof SpannedString) { + return (SpannedString) source; + } else { + return new SpannedString(source); + } + } +} diff --git a/core/java/android/text/StaticLayout.java b/core/java/android/text/StaticLayout.java new file mode 100644 index 0000000..2d18575 --- /dev/null +++ b/core/java/android/text/StaticLayout.java @@ -0,0 +1,1118 @@ +/* + * Copyright (C) 2006 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; + +import android.graphics.Paint; +import com.android.internal.util.ArrayUtils; +import android.util.Log; +import android.text.style.LeadingMarginSpan; +import android.text.style.LineHeightSpan; +import android.text.style.MetricAffectingSpan; +import android.text.style.ReplacementSpan; + +/** + * StaticLayout is a Layout for text that will not be edited after it + * is laid out. Use {@link DynamicLayout} for text that may change. + * <p>This is used by widgets to control text layout. You should not need + * to use this class directly unless you are implementing your own widget + * or custom display object, or would be tempted to call + * {@link android.graphics.Canvas#drawText(java.lang.CharSequence, int, int, float, float, android.graphics.Paint) + * Canvas.drawText()} directly.</p> + */ +public class +StaticLayout +extends Layout +{ + public StaticLayout(CharSequence source, TextPaint paint, + int width, + Alignment align, float spacingmult, float spacingadd, + boolean includepad) { + this(source, 0, source.length(), paint, width, align, + spacingmult, spacingadd, includepad); + } + + public StaticLayout(CharSequence source, int bufstart, int bufend, + TextPaint paint, int outerwidth, + Alignment align, + float spacingmult, float spacingadd, + boolean includepad) { + this(source, bufstart, bufend, paint, outerwidth, align, + spacingmult, spacingadd, includepad, null, 0); + } + + public StaticLayout(CharSequence source, int bufstart, int bufend, + TextPaint paint, int outerwidth, + Alignment align, + float spacingmult, float spacingadd, + boolean includepad, + TextUtils.TruncateAt ellipsize, int ellipsizedWidth) { + super((ellipsize == null) + ? source + : (source instanceof Spanned) + ? new SpannedEllipsizer(source) + : new Ellipsizer(source), + paint, outerwidth, align, spacingmult, spacingadd); + + /* + * This is annoying, but we can't refer to the layout until + * superclass construction is finished, and the superclass + * constructor wants the reference to the display text. + * + * This will break if the superclass constructor ever actually + * cares about the content instead of just holding the reference. + */ + if (ellipsize != null) { + Ellipsizer e = (Ellipsizer) getText(); + + e.mLayout = this; + e.mWidth = ellipsizedWidth; + e.mMethod = ellipsize; + mEllipsizedWidth = ellipsizedWidth; + + mColumns = COLUMNS_ELLIPSIZE; + } else { + mColumns = COLUMNS_NORMAL; + mEllipsizedWidth = outerwidth; + } + + mLines = new int[ArrayUtils.idealIntArraySize(2 * mColumns)]; + mLineDirections = new Directions[ + ArrayUtils.idealIntArraySize(2 * mColumns)]; + + generate(source, bufstart, bufend, paint, outerwidth, align, + spacingmult, spacingadd, includepad, includepad, + ellipsize != null, ellipsizedWidth, ellipsize); + + mChdirs = null; + mChs = null; + mWidths = null; + mFontMetricsInt = null; + } + + /* package */ StaticLayout(boolean ellipsize) { + super(null, null, 0, null, 0, 0); + + mColumns = COLUMNS_ELLIPSIZE; + mLines = new int[ArrayUtils.idealIntArraySize(2 * mColumns)]; + mLineDirections = new Directions[ + ArrayUtils.idealIntArraySize(2 * mColumns)]; + } + + /* package */ void generate(CharSequence source, int bufstart, int bufend, + TextPaint paint, int outerwidth, + Alignment align, + float spacingmult, float spacingadd, + boolean includepad, boolean trackpad, + boolean breakOnlyAtSpaces, + float ellipsizedWidth, TextUtils.TruncateAt where) { + mLineCount = 0; + + int v = 0; + boolean needMultiply = (spacingmult != 1 || spacingadd != 0); + + Paint.FontMetricsInt fm = mFontMetricsInt; + int[] choosehtv = null; + + int end = TextUtils.indexOf(source, '\n', bufstart, bufend); + int bufsiz = end >= 0 ? end - bufstart : bufend - bufstart; + boolean first = true; + + if (mChdirs == null) { + mChdirs = new byte[ArrayUtils.idealByteArraySize(bufsiz + 1)]; + mChs = new char[ArrayUtils.idealCharArraySize(bufsiz + 1)]; + mWidths = new float[ArrayUtils.idealIntArraySize((bufsiz + 1) * 2)]; + } + + byte[] chdirs = mChdirs; + char[] chs = mChs; + float[] widths = mWidths; + + AlteredCharSequence alter = null; + Spanned spanned = null; + + if (source instanceof Spanned) + spanned = (Spanned) source; + + int DEFAULT_DIR = DIR_LEFT_TO_RIGHT; // XXX + + for (int start = bufstart; start <= bufend; start = end) { + if (first) + first = false; + else + end = TextUtils.indexOf(source, '\n', start, bufend); + + if (end < 0) + end = bufend; + else + end++; + + int firstwidth = outerwidth; + int restwidth = outerwidth; + + LineHeightSpan[] chooseht = null; + + if (spanned != null) { + LeadingMarginSpan[] sp; + + sp = spanned.getSpans(start, end, LeadingMarginSpan.class); + for (int i = 0; i < sp.length; i++) { + firstwidth -= sp[i].getLeadingMargin(true); + restwidth -= sp[i].getLeadingMargin(false); + } + + chooseht = spanned.getSpans(start, end, LineHeightSpan.class); + + if (chooseht.length != 0) { + if (choosehtv == null || + choosehtv.length < chooseht.length) { + choosehtv = new int[ArrayUtils.idealIntArraySize( + chooseht.length)]; + } + + for (int i = 0; i < chooseht.length; i++) { + int o = spanned.getSpanStart(chooseht[i]); + + if (o < start) { + // starts in this layout, before the + // current paragraph + + choosehtv[i] = getLineTop(getLineForOffset(o)); + } else { + // starts in this paragraph + + choosehtv[i] = v; + } + } + } + } + + if (end - start > chdirs.length) { + chdirs = new byte[ArrayUtils.idealByteArraySize(end - start)]; + mChdirs = chdirs; + } + if (end - start > chs.length) { + chs = new char[ArrayUtils.idealCharArraySize(end - start)]; + mChs = chs; + } + if ((end - start) * 2 > widths.length) { + widths = new float[ArrayUtils.idealIntArraySize((end - start) * 2)]; + mWidths = widths; + } + + TextUtils.getChars(source, start, end, chs, 0); + final int n = end - start; + + boolean easy = true; + boolean altered = false; + int dir = DEFAULT_DIR; // XXX + + for (int i = 0; i < n; i++) { + if (chs[i] >= FIRST_RIGHT_TO_LEFT) { + easy = false; + break; + } + } + + if (!easy) { + AndroidCharacter.getDirectionalities(chs, chdirs, end - start); + + /* + * Determine primary paragraph direction + */ + + for (int j = start; j < end; j++) { + int d = chdirs[j - start]; + + if (d == Character.DIRECTIONALITY_LEFT_TO_RIGHT) { + dir = DIR_LEFT_TO_RIGHT; + break; + } + if (d == Character.DIRECTIONALITY_RIGHT_TO_LEFT) { + dir = DIR_RIGHT_TO_LEFT; + break; + } + } + + /* + * XXX Explicit overrides should go here + */ + + /* + * Weak type resolution + */ + + final byte SOR = dir == DIR_LEFT_TO_RIGHT ? + Character.DIRECTIONALITY_LEFT_TO_RIGHT : + Character.DIRECTIONALITY_RIGHT_TO_LEFT; + + // dump(chdirs, n, "initial"); + + // W1 non spacing marks + for (int j = 0; j < n; j++) { + if (chdirs[j] == Character.NON_SPACING_MARK) { + if (j == 0) + chdirs[j] = SOR; + else + chdirs[j] = chdirs[j - 1]; + } + } + + // dump(chdirs, n, "W1"); + + // W2 european numbers + byte cur = SOR; + for (int j = 0; j < n; j++) { + byte d = chdirs[j]; + + if (d == Character.DIRECTIONALITY_LEFT_TO_RIGHT || + d == Character.DIRECTIONALITY_RIGHT_TO_LEFT || + d == Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC) + cur = d; + else if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER) { + if (cur == + Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC) + chdirs[j] = Character.DIRECTIONALITY_ARABIC_NUMBER; + } + } + + // dump(chdirs, n, "W2"); + + // W3 arabic letters + for (int j = 0; j < n; j++) { + if (chdirs[j] == Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC) + chdirs[j] = Character.DIRECTIONALITY_RIGHT_TO_LEFT; + } + + // dump(chdirs, n, "W3"); + + // W4 single separator between numbers + for (int j = 1; j < n - 1; j++) { + byte d = chdirs[j]; + byte prev = chdirs[j - 1]; + byte next = chdirs[j + 1]; + + if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER_SEPARATOR) { + if (prev == Character.DIRECTIONALITY_EUROPEAN_NUMBER && + next == Character.DIRECTIONALITY_EUROPEAN_NUMBER) + chdirs[j] = Character.DIRECTIONALITY_EUROPEAN_NUMBER; + } else if (d == Character.DIRECTIONALITY_COMMON_NUMBER_SEPARATOR) { + if (prev == Character.DIRECTIONALITY_EUROPEAN_NUMBER && + next == Character.DIRECTIONALITY_EUROPEAN_NUMBER) + chdirs[j] = Character.DIRECTIONALITY_EUROPEAN_NUMBER; + if (prev == Character.DIRECTIONALITY_ARABIC_NUMBER && + next == Character.DIRECTIONALITY_ARABIC_NUMBER) + chdirs[j] = Character.DIRECTIONALITY_ARABIC_NUMBER; + } + } + + // dump(chdirs, n, "W4"); + + // W5 european number terminators + boolean adjacent = false; + for (int j = 0; j < n; j++) { + byte d = chdirs[j]; + + if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER) + adjacent = true; + else if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER_TERMINATOR && adjacent) + chdirs[j] = Character.DIRECTIONALITY_EUROPEAN_NUMBER; + else + adjacent = false; + } + + //dump(chdirs, n, "W5"); + + // W5 european number terminators part 2, + // W6 separators and terminators + adjacent = false; + for (int j = n - 1; j >= 0; j--) { + byte d = chdirs[j]; + + if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER) + adjacent = true; + else if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER_TERMINATOR) { + if (adjacent) + chdirs[j] = Character.DIRECTIONALITY_EUROPEAN_NUMBER; + else + chdirs[j] = Character.DIRECTIONALITY_OTHER_NEUTRALS; + } + else { + adjacent = false; + + if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER_SEPARATOR || + d == Character.DIRECTIONALITY_COMMON_NUMBER_SEPARATOR || + d == Character.DIRECTIONALITY_PARAGRAPH_SEPARATOR || + d == Character.DIRECTIONALITY_SEGMENT_SEPARATOR) + chdirs[j] = Character.DIRECTIONALITY_OTHER_NEUTRALS; + } + } + + // dump(chdirs, n, "W6"); + + // W7 strong direction of european numbers + cur = SOR; + for (int j = 0; j < n; j++) { + byte d = chdirs[j]; + + if (d == SOR || + d == Character.DIRECTIONALITY_LEFT_TO_RIGHT || + d == Character.DIRECTIONALITY_RIGHT_TO_LEFT) + cur = d; + + if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER) + chdirs[j] = cur; + } + + // dump(chdirs, n, "W7"); + + // N1, N2 neutrals + cur = SOR; + for (int j = 0; j < n; j++) { + byte d = chdirs[j]; + + if (d == Character.DIRECTIONALITY_LEFT_TO_RIGHT || + d == Character.DIRECTIONALITY_RIGHT_TO_LEFT) { + cur = d; + } else if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER || + d == Character.DIRECTIONALITY_ARABIC_NUMBER) { + cur = Character.DIRECTIONALITY_RIGHT_TO_LEFT; + } else { + byte dd = SOR; + int k; + + for (k = j + 1; k < n; k++) { + dd = chdirs[k]; + + if (dd == Character.DIRECTIONALITY_LEFT_TO_RIGHT || + dd == Character.DIRECTIONALITY_RIGHT_TO_LEFT) { + break; + } + if (dd == Character.DIRECTIONALITY_EUROPEAN_NUMBER || + dd == Character.DIRECTIONALITY_ARABIC_NUMBER) { + dd = Character.DIRECTIONALITY_RIGHT_TO_LEFT; + break; + } + } + + for (int y = j; y < k; y++) { + if (dd == cur) + chdirs[y] = cur; + else + chdirs[y] = SOR; + } + + j = k - 1; + } + } + + // dump(chdirs, n, "final"); + + // extra: enforce that all tabs go the primary direction + + for (int j = 0; j < n; j++) { + if (chs[j] == '\t') + chdirs[j] = SOR; + } + + // extra: enforce that object replacements go to the + // primary direction + // and that none of the underlying characters are treated + // as viable breakpoints + + if (source instanceof Spanned) { + Spanned sp = (Spanned) source; + ReplacementSpan[] spans = sp.getSpans(start, end, ReplacementSpan.class); + + for (int y = 0; y < spans.length; y++) { + int a = sp.getSpanStart(spans[y]); + int b = sp.getSpanEnd(spans[y]); + + for (int x = a; x < b; x++) { + chdirs[x - start] = SOR; + chs[x - start] = '\uFFFC'; + } + } + } + + // Do mirroring for right-to-left segments + + for (int i = 0; i < n; i++) { + if (chdirs[i] == Character.DIRECTIONALITY_RIGHT_TO_LEFT) { + int j; + + for (j = i; j < n; j++) { + if (chdirs[j] != + Character.DIRECTIONALITY_RIGHT_TO_LEFT) + break; + } + + if (AndroidCharacter.mirror(chs, i, j - i)) + altered = true; + + i = j - 1; + } + } + } + + CharSequence sub; + + if (altered) { + if (alter == null) + alter = AlteredCharSequence.make(source, chs, start, end); + else + alter.update(chs, start, end); + + sub = alter; + } else { + sub = source; + } + + int width = firstwidth; + + float w = 0; + int here = start; + + int ok = start; + float okwidth = w; + int okascent = 0, okdescent = 0, oktop = 0, okbottom = 0; + + int fit = start; + float fitwidth = w; + int fitascent = 0, fitdescent = 0, fittop = 0, fitbottom = 0; + + boolean tab = false; + + int next; + for (int i = start; i < end; i = next) { + if (spanned == null) + next = end; + else + next = spanned.nextSpanTransition(i, end, + MetricAffectingSpan. + class); + + if (spanned == null) { + paint.getTextWidths(sub, i, next, widths); + System.arraycopy(widths, 0, widths, + end - start + (i - start), next - i); + + paint.getFontMetricsInt(fm); + } else { + mWorkPaint.baselineShift = 0; + + Styled.getTextWidths(paint, mWorkPaint, + spanned, i, next, + widths, fm); + System.arraycopy(widths, 0, widths, + end - start + (i - start), next - i); + + if (mWorkPaint.baselineShift < 0) { + fm.ascent += mWorkPaint.baselineShift; + fm.top += mWorkPaint.baselineShift; + } else { + fm.descent += mWorkPaint.baselineShift; + fm.bottom += mWorkPaint.baselineShift; + } + } + + int fmtop = fm.top; + int fmbottom = fm.bottom; + int fmascent = fm.ascent; + int fmdescent = fm.descent; + + if (false) { + StringBuilder sb = new StringBuilder(); + for (int j = i; j < next; j++) { + sb.append(widths[j - start + (end - start)]); + sb.append(' '); + } + + Log.e("text", sb.toString()); + } + + for (int j = i; j < next; j++) { + char c = chs[j - start]; + float before = w; + + switch (c) { + case '\n': + break; + + case '\t': + w = Layout.nextTab(sub, start, end, w, null); + tab = true; + break; + + default: + w += widths[j - start + (end - start)]; + } + + // Log.e("text", "was " + before + " now " + w + " after " + c + " within " + width); + + if (w <= width) { + fitwidth = w; + fit = j + 1; + + if (fmtop < fittop) + fittop = fmtop; + if (fmascent < fitascent) + fitascent = fmascent; + if (fmdescent > fitdescent) + fitdescent = fmdescent; + if (fmbottom > fitbottom) + fitbottom = fmbottom; + + if (c == ' ' || c == '\t') { + okwidth = w; + ok = j + 1; + + if (fittop < oktop) + oktop = fittop; + if (fitascent < okascent) + okascent = fitascent; + if (fitdescent > okdescent) + okdescent = fitdescent; + if (fitbottom > okbottom) + okbottom = fitbottom; + } + } else if (breakOnlyAtSpaces) { + if (ok != here) { + // Log.e("text", "output ok " + here + " to " +ok); + v = out(source, + here, ok, + okascent, okdescent, oktop, okbottom, + v, + spacingmult, spacingadd, chooseht, + choosehtv, fm, tab, + needMultiply, start, chdirs, dir, easy, + ok == bufend, includepad, trackpad, + widths, start, end - start, + where, ellipsizedWidth, okwidth, + paint); + + here = ok; + } else { + // Act like it fit even though it didn't. + + fitwidth = w; + fit = j + 1; + + if (fmtop < fittop) + fittop = fmtop; + if (fmascent < fitascent) + fitascent = fmascent; + if (fmdescent > fitdescent) + fitdescent = fmdescent; + if (fmbottom > fitbottom) + fitbottom = fmbottom; + } + } else { + if (ok != here) { + // Log.e("text", "output ok " + here + " to " +ok); + v = out(source, + here, ok, + okascent, okdescent, oktop, okbottom, + v, + spacingmult, spacingadd, chooseht, + choosehtv, fm, tab, + needMultiply, start, chdirs, dir, easy, + ok == bufend, includepad, trackpad, + widths, start, end - start, + where, ellipsizedWidth, okwidth, + paint); + + here = ok; + } else if (fit != here) { + // Log.e("text", "output fit " + here + " to " +fit); + v = out(source, + here, fit, + fitascent, fitdescent, + fittop, fitbottom, + v, + spacingmult, spacingadd, chooseht, + choosehtv, fm, tab, + needMultiply, start, chdirs, dir, easy, + fit == bufend, includepad, trackpad, + widths, start, end - start, + where, ellipsizedWidth, fitwidth, + paint); + + here = fit; + } else { + // Log.e("text", "output one " + here + " to " +(here + 1)); + measureText(paint, mWorkPaint, + source, here, here + 1, fm, tab, + null); + + v = out(source, + here, here+1, + fm.ascent, fm.descent, + fm.top, fm.bottom, + v, + spacingmult, spacingadd, chooseht, + choosehtv, fm, tab, + needMultiply, start, chdirs, dir, easy, + here + 1 == bufend, includepad, + trackpad, + widths, start, end - start, + where, ellipsizedWidth, + widths[here - start], paint); + + here = here + 1; + } + + if (here < i) { + j = next = here; // must remeasure + } else { + j = here - 1; // continue looping + } + + ok = fit = here; + w = 0; + fitascent = fitdescent = fittop = fitbottom = 0; + okascent = okdescent = oktop = okbottom = 0; + + width = restwidth; + } + } + } + + if (end != here) { + if ((fittop | fitbottom | fitdescent | fitascent) == 0) { + paint.getFontMetricsInt(fm); + + fittop = fm.top; + fitbottom = fm.bottom; + fitascent = fm.ascent; + fitdescent = fm.descent; + } + + // Log.e("text", "output rest " + here + " to " + end); + + v = out(source, + here, end, fitascent, fitdescent, + fittop, fitbottom, + v, + spacingmult, spacingadd, chooseht, + choosehtv, fm, tab, + needMultiply, start, chdirs, dir, easy, + end == bufend, includepad, trackpad, + widths, start, end - start, + where, ellipsizedWidth, w, paint); + } + + start = end; + + if (end == bufend) + break; + } + + if (bufend == bufstart || source.charAt(bufend - 1) == '\n') { + // Log.e("text", "output last " + bufend); + + paint.getFontMetricsInt(fm); + + v = out(source, + bufend, bufend, fm.ascent, fm.descent, + fm.top, fm.bottom, + v, + spacingmult, spacingadd, null, + null, fm, false, + needMultiply, bufend, chdirs, DEFAULT_DIR, true, + true, includepad, trackpad, + widths, bufstart, 0, + where, ellipsizedWidth, 0, paint); + } + } + +/* + private static void dump(byte[] data, int count, String label) { + if (false) { + System.out.print(label); + + for (int i = 0; i < count; i++) + System.out.print(" " + data[i]); + + System.out.println(); + } + } +*/ + + private static int getFit(TextPaint paint, + TextPaint workPaint, + CharSequence text, int start, int end, + float wid) { + int high = end + 1, low = start - 1, guess; + + while (high - low > 1) { + guess = (high + low) / 2; + + if (measureText(paint, workPaint, + text, start, guess, null, true, null) > wid) + high = guess; + else + low = guess; + } + + if (low < start) + return start; + else + return low; + } + + private int out(CharSequence text, int start, int end, + int above, int below, int top, int bottom, int v, + float spacingmult, float spacingadd, + LineHeightSpan[] chooseht, int[] choosehtv, + Paint.FontMetricsInt fm, boolean tab, + boolean needMultiply, int pstart, byte[] chdirs, + int dir, boolean easy, boolean last, + boolean includepad, boolean trackpad, + float[] widths, int widstart, int widoff, + TextUtils.TruncateAt ellipsize, float ellipsiswidth, + float textwidth, TextPaint paint) { + int j = mLineCount; + int off = j * mColumns; + int want = off + mColumns + TOP; + int[] lines = mLines; + + // Log.e("text", "line " + start + " to " + end + (last ? "===" : "")); + + if (want >= lines.length) { + int nlen = ArrayUtils.idealIntArraySize(want + 1); + int[] grow = new int[nlen]; + System.arraycopy(lines, 0, grow, 0, lines.length); + mLines = grow; + lines = grow; + + Directions[] grow2 = new Directions[nlen]; + System.arraycopy(mLineDirections, 0, grow2, 0, + mLineDirections.length); + mLineDirections = grow2; + } + + if (chooseht != null) { + fm.ascent = above; + fm.descent = below; + fm.top = top; + fm.bottom = bottom; + + for (int i = 0; i < chooseht.length; i++) { + chooseht[i].chooseHeight(text, start, end, choosehtv[i], v, fm); + } + + above = fm.ascent; + below = fm.descent; + top = fm.top; + bottom = fm.bottom; + } + + if (j == 0) { + if (trackpad) { + mTopPadding = top - above; + } + + if (includepad) { + above = top; + } + } + if (last) { + if (trackpad) { + mBottomPadding = bottom - below; + } + + if (includepad) { + below = bottom; + } + } + + int extra; + + if (needMultiply) { + extra = (int) ((below - above) * (spacingmult - 1) + + spacingadd + 0.5); + } else { + extra = 0; + } + + lines[off + START] = start; + lines[off + TOP] = v; + lines[off + DESCENT] = below + extra; + + v += (below - above) + extra; + lines[off + mColumns + START] = end; + lines[off + mColumns + TOP] = v; + + if (tab) + lines[off + TAB] |= TAB_MASK; + + { + lines[off + DIR] |= dir << DIR_SHIFT; + + int cur = Character.DIRECTIONALITY_LEFT_TO_RIGHT; + int count = 0; + + if (!easy) { + for (int k = start; k < end; k++) { + if (chdirs[k - pstart] != cur) { + count++; + cur = chdirs[k - pstart]; + } + } + } + + Directions linedirs; + + if (count == 0) { + linedirs = DIRS_ALL_LEFT_TO_RIGHT; + } else { + short[] ld = new short[count + 1]; + + cur = Character.DIRECTIONALITY_LEFT_TO_RIGHT; + count = 0; + int here = start; + + for (int k = start; k < end; k++) { + if (chdirs[k - pstart] != cur) { + // XXX check to make sure we don't + // overflow short + ld[count++] = (short) (k - here); + cur = chdirs[k - pstart]; + here = k; + } + } + + ld[count] = (short) (end - here); + + if (count == 1 && ld[0] == 0) { + linedirs = DIRS_ALL_RIGHT_TO_LEFT; + } else { + linedirs = new Directions(ld); + } + } + + mLineDirections[j] = linedirs; + + if (ellipsize != null) { + calculateEllipsis(start, end, widths, widstart, widoff, + ellipsiswidth, ellipsize, j, + textwidth, paint); + } + } + + mLineCount++; + return v; + } + + private void calculateEllipsis(int linestart, int lineend, + float[] widths, int widstart, int widoff, + float avail, TextUtils.TruncateAt where, + int line, float textwidth, TextPaint paint) { + int len = lineend - linestart; + + if (textwidth <= avail) { + // Everything fits! + mLines[mColumns * line + ELLIPSIS_START] = 0; + mLines[mColumns * line + ELLIPSIS_COUNT] = 0; + return; + } + + float ellipsiswid = paint.measureText("\u2026"); + int ellipsisStart, ellipsisCount; + + if (where == TextUtils.TruncateAt.START) { + float sum = 0; + int i; + + for (i = len; i >= 0; i--) { + float w = widths[i - 1 + linestart - widstart + widoff]; + + if (w + sum + ellipsiswid > avail) { + break; + } + + sum += w; + } + + ellipsisStart = 0; + ellipsisCount = i; + } else if (where == TextUtils.TruncateAt.END) { + float sum = 0; + int i; + + for (i = 0; i < len; i++) { + float w = widths[i + linestart - widstart + widoff]; + + if (w + sum + ellipsiswid > avail) { + break; + } + + sum += w; + } + + ellipsisStart = i; + ellipsisCount = len - i; + } else /* where = TextUtils.TruncateAt.MIDDLE */ { + float lsum = 0, rsum = 0; + int left = 0, right = len; + + float ravail = (avail - ellipsiswid) / 2; + for (right = len; right >= 0; right--) { + float w = widths[right - 1 + linestart - widstart + widoff]; + + if (w + rsum > ravail) { + break; + } + + rsum += w; + } + + float lavail = avail - ellipsiswid - rsum; + for (left = 0; left < right; left++) { + float w = widths[left + linestart - widstart + widoff]; + + if (w + lsum > lavail) { + break; + } + + lsum += w; + } + + ellipsisStart = left; + ellipsisCount = right - left; + } + + mLines[mColumns * line + ELLIPSIS_START] = ellipsisStart; + mLines[mColumns * line + ELLIPSIS_COUNT] = ellipsisCount; + } + + // Override the baseclass so we can directly access our members, + // rather than relying on member functions. + // The logic mirrors that of Layout.getLineForVertical + // FIXME: It may be faster to do a linear search for layouts without many lines. + public int getLineForVertical(int vertical) { + int high = mLineCount; + int low = -1; + int guess; + int[] lines = mLines; + while (high - low > 1) { + guess = (high + low) >> 1; + if (lines[mColumns * guess + TOP] > vertical){ + high = guess; + } else { + low = guess; + } + } + if (low < 0) { + return 0; + } else { + return low; + } + } + + public int getLineCount() { + return mLineCount; + } + + public int getLineTop(int line) { + return mLines[mColumns * line + TOP]; + } + + public int getLineDescent(int line) { + return mLines[mColumns * line + DESCENT]; + } + + public int getLineStart(int line) { + return mLines[mColumns * line + START] & START_MASK; + } + + public int getParagraphDirection(int line) { + return mLines[mColumns * line + DIR] >> DIR_SHIFT; + } + + public boolean getLineContainsTab(int line) { + return (mLines[mColumns * line + TAB] & TAB_MASK) != 0; + } + + public final Directions getLineDirections(int line) { + return mLineDirections[line]; + } + + public int getTopPadding() { + return mTopPadding; + } + + public int getBottomPadding() { + return mBottomPadding; + } + + @Override + public int getEllipsisCount(int line) { + if (mColumns < COLUMNS_ELLIPSIZE) { + return 0; + } + + return mLines[mColumns * line + ELLIPSIS_COUNT]; + } + + @Override + public int getEllipsisStart(int line) { + if (mColumns < COLUMNS_ELLIPSIZE) { + return 0; + } + + return mLines[mColumns * line + ELLIPSIS_START]; + } + + @Override + public int getEllipsizedWidth() { + return mEllipsizedWidth; + } + + private int mLineCount; + private int mTopPadding, mBottomPadding; + private int mColumns; + private int mEllipsizedWidth; + + private static final int COLUMNS_NORMAL = 3; + private static final int COLUMNS_ELLIPSIZE = 5; + private static final int START = 0; + private static final int DIR = START; + private static final int TAB = START; + private static final int TOP = 1; + private static final int DESCENT = 2; + private static final int ELLIPSIS_START = 3; + private static final int ELLIPSIS_COUNT = 4; + + private int[] mLines; + private Directions[] mLineDirections; + + private static final int START_MASK = 0x1FFFFFFF; + private static final int DIR_MASK = 0xC0000000; + private static final int DIR_SHIFT = 30; + private static final int TAB_MASK = 0x20000000; + + private static final char FIRST_RIGHT_TO_LEFT = '\u0590'; + + /* + * These are reused across calls to generate() + */ + private byte[] mChdirs; + private char[] mChs; + private float[] mWidths; + private Paint.FontMetricsInt mFontMetricsInt = new Paint.FontMetricsInt(); +} diff --git a/core/java/android/text/Styled.java b/core/java/android/text/Styled.java new file mode 100644 index 0000000..05c27ea --- /dev/null +++ b/core/java/android/text/Styled.java @@ -0,0 +1,298 @@ +/* + * Copyright (C) 2006 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; + +import android.graphics.Paint; +import android.graphics.Canvas; +import android.graphics.Path; +import android.graphics.RectF; +import android.graphics.Typeface; +import android.graphics.MaskFilter; +import android.graphics.Rasterizer; +import android.graphics.LayerRasterizer; +import android.text.style.*; + +/* package */ class Styled +{ + private static float each(Canvas canvas, + Spanned text, int start, int end, + int dir, boolean reverse, + float x, int top, int y, int bottom, + Paint.FontMetricsInt fm, + TextPaint realPaint, + TextPaint paint, + boolean needwid) { + + boolean havewid = false; + float ret = 0; + CharacterStyle[] spans = text.getSpans(start, end, CharacterStyle.class); + + ReplacementSpan replacement = null; + + realPaint.bgColor = 0; + realPaint.baselineShift = 0; + paint.set(realPaint); + + if (spans.length > 0) { + for (int i = 0; i < spans.length; i++) { + CharacterStyle span = spans[i]; + + if (span instanceof ReplacementSpan) { + replacement = (ReplacementSpan)span; + } + else { + span.updateDrawState(paint); + } + } + } + + if (replacement == null) { + CharSequence tmp; + int tmpstart, tmpend; + + if (reverse) { + tmp = TextUtils.getReverse(text, start, end); + tmpstart = 0; + tmpend = end - start; + } else { + tmp = text; + tmpstart = start; + tmpend = end; + } + + if (fm != null) { + paint.getFontMetricsInt(fm); + } + + if (canvas != null) { + if (paint.bgColor != 0) { + int c = paint.getColor(); + Paint.Style s = paint.getStyle(); + paint.setColor(paint.bgColor); + paint.setStyle(Paint.Style.FILL); + + if (!havewid) { + ret = paint.measureText(tmp, tmpstart, tmpend); + havewid = true; + } + + if (dir == Layout.DIR_RIGHT_TO_LEFT) + canvas.drawRect(x - ret, top, x, bottom, paint); + else + canvas.drawRect(x, top, x + ret, bottom, paint); + + paint.setStyle(s); + paint.setColor(c); + } + + if (dir == Layout.DIR_RIGHT_TO_LEFT) { + if (!havewid) { + ret = paint.measureText(tmp, tmpstart, tmpend); + havewid = true; + } + + canvas.drawText(tmp, tmpstart, tmpend, + x - ret, y + paint.baselineShift, paint); + } else { + if (needwid) { + if (!havewid) { + ret = paint.measureText(tmp, tmpstart, tmpend); + havewid = true; + } + } + + canvas.drawText(tmp, tmpstart, tmpend, + x, y + paint.baselineShift, paint); + } + } else { + if (needwid && !havewid) { + ret = paint.measureText(tmp, tmpstart, tmpend); + havewid = true; + } + } + } else { + ret = replacement.getSize(paint, text, start, end, fm); + + if (canvas != null) { + if (dir == Layout.DIR_RIGHT_TO_LEFT) + replacement.draw(canvas, text, start, end, + x - ret, top, y, bottom, paint); + else + replacement.draw(canvas, text, start, end, + x, top, y, bottom, paint); + } + } + + if (dir == Layout.DIR_RIGHT_TO_LEFT) + return -ret; + else + return ret; + } + + public static int getTextWidths(TextPaint realPaint, + TextPaint paint, + Spanned text, int start, int end, + float[] widths, Paint.FontMetricsInt fm) { + + MetricAffectingSpan[] spans = text.getSpans(start, end, MetricAffectingSpan.class); + + ReplacementSpan replacement = null; + paint.set(realPaint); + + for (int i = 0; i < spans.length; i++) { + MetricAffectingSpan span = spans[i]; + if (span instanceof ReplacementSpan) { + replacement = (ReplacementSpan)span; + } + else { + span.updateMeasureState(paint); + } + } + + if (replacement == null) { + paint.getFontMetricsInt(fm); + paint.getTextWidths(text, start, end, widths); + } else { + int wid = replacement.getSize(paint, text, start, end, fm); + + if (end > start) { + widths[0] = wid; + + for (int i = start + 1; i < end; i++) + widths[i - start] = 0; + } + } + return end - start; + } + + private static float foreach(Canvas canvas, + CharSequence text, int start, int end, + int dir, boolean reverse, + float x, int top, int y, int bottom, + Paint.FontMetricsInt fm, + TextPaint paint, + TextPaint workPaint, + boolean needwid) { + if (! (text instanceof Spanned)) { + float ret = 0; + + if (reverse) { + CharSequence tmp = TextUtils.getReverse(text, start, end); + int tmpend = end - start; + + if (canvas != null || needwid) + ret = paint.measureText(tmp, 0, tmpend); + + if (canvas != null) + canvas.drawText(tmp, 0, tmpend, + x - ret, y, paint); + } else { + if (needwid) + ret = paint.measureText(text, start, end); + + if (canvas != null) + canvas.drawText(text, start, end, x, y, paint); + } + + if (fm != null) { + paint.getFontMetricsInt(fm); + } + + return ret * dir; //Layout.DIR_RIGHT_TO_LEFT == -1 + } + + float ox = x; + int asc = 0, desc = 0; + int ftop = 0, fbot = 0; + + Spanned sp = (Spanned) text; + Class division; + + if (canvas == null) + division = MetricAffectingSpan.class; + else + division = CharacterStyle.class; + + int next; + for (int i = start; i < end; i = next) { + next = sp.nextSpanTransition(i, end, division); + + x += each(canvas, sp, i, next, dir, reverse, + x, top, y, bottom, fm, paint, workPaint, + needwid || next != end); + + if (fm != null) { + if (fm.ascent < asc) + asc = fm.ascent; + if (fm.descent > desc) + desc = fm.descent; + + if (fm.top < ftop) + ftop = fm.top; + if (fm.bottom > fbot) + fbot = fm.bottom; + } + } + + if (fm != null) { + if (start == end) { + paint.getFontMetricsInt(fm); + } else { + fm.ascent = asc; + fm.descent = desc; + fm.top = ftop; + fm.bottom = fbot; + } + } + + return x - ox; + } + + public static float drawText(Canvas canvas, + CharSequence text, int start, int end, + int dir, boolean reverse, + float x, int top, int y, int bottom, + TextPaint paint, + TextPaint workPaint, + boolean needwid) { + if ((dir == Layout.DIR_RIGHT_TO_LEFT && !reverse)||(reverse && dir == Layout.DIR_LEFT_TO_RIGHT)) { + float ch = foreach(null, text, start, end, Layout.DIR_LEFT_TO_RIGHT, + false, 0, 0, 0, 0, null, paint, workPaint, + true); + + ch *= dir; // DIR_RIGHT_TO_LEFT == -1 + foreach(canvas, text, start, end, -dir, + reverse, x + ch, top, y, bottom, null, paint, + workPaint, true); + + return ch; + } + + return foreach(canvas, text, start, end, dir, reverse, + x, top, y, bottom, null, paint, workPaint, + needwid); + } + + public static float measureText(TextPaint paint, + TextPaint workPaint, + CharSequence text, int start, int end, + Paint.FontMetricsInt fm) { + return foreach(null, text, start, end, + Layout.DIR_LEFT_TO_RIGHT, false, + 0, 0, 0, 0, fm, paint, workPaint, true); + } +} diff --git a/core/java/android/text/TextPaint.java b/core/java/android/text/TextPaint.java new file mode 100644 index 0000000..f13820d --- /dev/null +++ b/core/java/android/text/TextPaint.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2006 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; + +import android.graphics.Paint; + +/** + * TextPaint is an extension of Paint that leaves room for some extra + * data used during text measuring and drawing. + */ +public class TextPaint extends Paint { + public int bgColor; + public int baselineShift; + public int linkColor; + public int[] drawableState; + + public TextPaint() { + super(); + } + + public TextPaint(int flags) { + super(flags); + } + + public TextPaint(Paint p) { + super(p); + } + + /** + * Copy the fields from tp into this TextPaint, including the + * fields inherited from Paint. + */ + public void set(TextPaint tp) { + super.set(tp); + + bgColor = tp.bgColor; + baselineShift = tp.baselineShift; + linkColor = tp.linkColor; + drawableState = tp.drawableState; + } +} diff --git a/core/java/android/text/TextUtils.java b/core/java/android/text/TextUtils.java new file mode 100644 index 0000000..e791aaf --- /dev/null +++ b/core/java/android/text/TextUtils.java @@ -0,0 +1,1570 @@ +/* + * Copyright (C) 2006 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; + +import com.android.internal.R; + +import android.content.res.ColorStateList; +import android.content.res.Resources; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.style.AbsoluteSizeSpan; +import android.text.style.AlignmentSpan; +import android.text.style.BackgroundColorSpan; +import android.text.style.BulletSpan; +import android.text.style.CharacterStyle; +import android.text.style.ForegroundColorSpan; +import android.text.style.LeadingMarginSpan; +import android.text.style.MetricAffectingSpan; +import android.text.style.QuoteSpan; +import android.text.style.RelativeSizeSpan; +import android.text.style.ReplacementSpan; +import android.text.style.ScaleXSpan; +import android.text.style.StrikethroughSpan; +import android.text.style.StyleSpan; +import android.text.style.SubscriptSpan; +import android.text.style.SuperscriptSpan; +import android.text.style.TextAppearanceSpan; +import android.text.style.TypefaceSpan; +import android.text.style.URLSpan; +import android.text.style.UnderlineSpan; +import com.android.internal.util.ArrayUtils; + +import java.util.regex.Pattern; +import java.util.Iterator; + +public class TextUtils +{ + private TextUtils() { /* cannot be instantiated */ } + + private static String[] EMPTY_STRING_ARRAY = new String[]{}; + + public static void getChars(CharSequence s, int start, int end, + char[] dest, int destoff) { + Class c = s.getClass(); + + if (c == String.class) + ((String) s).getChars(start, end, dest, destoff); + else if (c == StringBuffer.class) + ((StringBuffer) s).getChars(start, end, dest, destoff); + else if (c == StringBuilder.class) + ((StringBuilder) s).getChars(start, end, dest, destoff); + else if (s instanceof GetChars) + ((GetChars) s).getChars(start, end, dest, destoff); + else { + for (int i = start; i < end; i++) + dest[destoff++] = s.charAt(i); + } + } + + public static int indexOf(CharSequence s, char ch) { + return indexOf(s, ch, 0); + } + + public static int indexOf(CharSequence s, char ch, int start) { + Class c = s.getClass(); + + if (c == String.class) + return ((String) s).indexOf(ch, start); + + return indexOf(s, ch, start, s.length()); + } + + public static int indexOf(CharSequence s, char ch, int start, int end) { + Class c = s.getClass(); + + if (s instanceof GetChars || c == StringBuffer.class || + c == StringBuilder.class || c == String.class) { + final int INDEX_INCREMENT = 500; + char[] temp = obtain(INDEX_INCREMENT); + + while (start < end) { + int segend = start + INDEX_INCREMENT; + if (segend > end) + segend = end; + + getChars(s, start, segend, temp, 0); + + int count = segend - start; + for (int i = 0; i < count; i++) { + if (temp[i] == ch) { + recycle(temp); + return i + start; + } + } + + start = segend; + } + + recycle(temp); + return -1; + } + + for (int i = start; i < end; i++) + if (s.charAt(i) == ch) + return i; + + return -1; + } + + public static int lastIndexOf(CharSequence s, char ch) { + return lastIndexOf(s, ch, s.length() - 1); + } + + public static int lastIndexOf(CharSequence s, char ch, int last) { + Class c = s.getClass(); + + if (c == String.class) + return ((String) s).lastIndexOf(ch, last); + + return lastIndexOf(s, ch, 0, last); + } + + public static int lastIndexOf(CharSequence s, char ch, + int start, int last) { + if (last < 0) + return -1; + if (last >= s.length()) + last = s.length() - 1; + + int end = last + 1; + + Class c = s.getClass(); + + if (s instanceof GetChars || c == StringBuffer.class || + c == StringBuilder.class || c == String.class) { + final int INDEX_INCREMENT = 500; + char[] temp = obtain(INDEX_INCREMENT); + + while (start < end) { + int segstart = end - INDEX_INCREMENT; + if (segstart < start) + segstart = start; + + getChars(s, segstart, end, temp, 0); + + int count = end - segstart; + for (int i = count - 1; i >= 0; i--) { + if (temp[i] == ch) { + recycle(temp); + return i + segstart; + } + } + + end = segstart; + } + + recycle(temp); + return -1; + } + + for (int i = end - 1; i >= start; i--) + if (s.charAt(i) == ch) + return i; + + return -1; + } + + public static int indexOf(CharSequence s, CharSequence needle) { + return indexOf(s, needle, 0, s.length()); + } + + public static int indexOf(CharSequence s, CharSequence needle, int start) { + return indexOf(s, needle, start, s.length()); + } + + public static int indexOf(CharSequence s, CharSequence needle, + int start, int end) { + int nlen = needle.length(); + if (nlen == 0) + return start; + + char c = needle.charAt(0); + + for (;;) { + start = indexOf(s, c, start); + if (start > end - nlen) { + break; + } + + if (start < 0) { + return -1; + } + + if (regionMatches(s, start, needle, 0, nlen)) { + return start; + } + + start++; + } + return -1; + } + + public static boolean regionMatches(CharSequence one, int toffset, + CharSequence two, int ooffset, + int len) { + char[] temp = obtain(2 * len); + + getChars(one, toffset, toffset + len, temp, 0); + getChars(two, ooffset, ooffset + len, temp, len); + + boolean match = true; + for (int i = 0; i < len; i++) { + if (temp[i] != temp[i + len]) { + match = false; + break; + } + } + + recycle(temp); + return match; + } + + public static String substring(CharSequence source, int start, int end) { + if (source instanceof String) + return ((String) source).substring(start, end); + if (source instanceof StringBuilder) + return ((StringBuilder) source).substring(start, end); + if (source instanceof StringBuffer) + return ((StringBuffer) source).substring(start, end); + + char[] temp = obtain(end - start); + getChars(source, start, end, temp, 0); + String ret = new String(temp, 0, end - start); + recycle(temp); + + return ret; + } + + /** + * Returns a string containing the tokens joined by delimiters. + * @param tokens an array objects to be joined. Strings will be formed from + * the objects by calling object.toString(). + */ + public static String join(CharSequence delimiter, Object[] tokens) { + StringBuilder sb = new StringBuilder(); + boolean firstTime = true; + for (Object token: tokens) { + if (firstTime) { + firstTime = false; + } else { + sb.append(delimiter); + } + sb.append(token); + } + return sb.toString(); + } + + /** + * Returns a string containing the tokens joined by delimiters. + * @param tokens an array objects to be joined. Strings will be formed from + * the objects by calling object.toString(). + */ + public static String join(CharSequence delimiter, Iterable tokens) { + StringBuilder sb = new StringBuilder(); + boolean firstTime = true; + for (Object token: tokens) { + if (firstTime) { + firstTime = false; + } else { + sb.append(delimiter); + } + sb.append(token); + } + return sb.toString(); + } + + /** + * String.split() returns [''] when the string to be split is empty. This returns []. This does + * not remove any empty strings from the result. For example split("a,", "," ) returns {"a", ""}. + * + * @param text the string to split + * @param expression the regular expression to match + * @return an array of strings. The array will be empty if text is empty + * + * @throws NullPointerException if expression or text is null + */ + public static String[] split(String text, String expression) { + if (text.length() == 0) { + return EMPTY_STRING_ARRAY; + } else { + return text.split(expression, -1); + } + } + + /** + * Splits a string on a pattern. String.split() returns [''] when the string to be + * split is empty. This returns []. This does not remove any empty strings from the result. + * @param text the string to split + * @param pattern the regular expression to match + * @return an array of strings. The array will be empty if text is empty + * + * @throws NullPointerException if expression or text is null + */ + public static String[] split(String text, Pattern pattern) { + if (text.length() == 0) { + return EMPTY_STRING_ARRAY; + } else { + return pattern.split(text, -1); + } + } + + /** + * An interface for splitting strings according to rules that are opaque to the user of this + * interface. This also has less overhead than split, which uses regular expressions and + * allocates an array to hold the results. + * + * <p>The most efficient way to use this class is: + * + * <pre> + * // Once + * TextUtils.StringSplitter splitter = new TextUtils.SimpleStringSplitter(delimiter); + * + * // Once per string to split + * splitter.setString(string); + * for (String s : splitter) { + * ... + * } + * </pre> + */ + public interface StringSplitter extends Iterable<String> { + public void setString(String string); + } + + /** + * A simple string splitter. + * + * <p>If the final character in the string to split is the delimiter then no empty string will + * be returned for the empty string after that delimeter. That is, splitting <tt>"a,b,"</tt> on + * comma will return <tt>"a", "b"</tt>, not <tt>"a", "b", ""</tt>. + */ + public static class SimpleStringSplitter implements StringSplitter, Iterator<String> { + private String mString; + private char mDelimiter; + private int mPosition; + private int mLength; + + /** + * Initializes the splitter. setString may be called later. + * @param delimiter the delimeter on which to split + */ + public SimpleStringSplitter(char delimiter) { + mDelimiter = delimiter; + } + + /** + * Sets the string to split + * @param string the string to split + */ + public void setString(String string) { + mString = string; + mPosition = 0; + mLength = mString.length(); + } + + public Iterator<String> iterator() { + return this; + } + + public boolean hasNext() { + return mPosition < mLength; + } + + public String next() { + int end = mString.indexOf(mDelimiter, mPosition); + if (end == -1) { + end = mLength; + } + String nextString = mString.substring(mPosition, end); + mPosition = end + 1; // Skip the delimiter. + return nextString; + } + + public void remove() { + throw new UnsupportedOperationException(); + } + } + + public static CharSequence stringOrSpannedString(CharSequence source) { + if (source == null) + return null; + if (source instanceof SpannedString) + return source; + if (source instanceof Spanned) + return new SpannedString(source); + + return source.toString(); + } + + /** + * Returns true if the string is null or 0-length. + * @param str the string to be examined + * @return true if str is null or zero length + */ + public static boolean isEmpty(CharSequence str) { + if (str == null || str.length() == 0) + return true; + else + return false; + } + + /** + * Returns the length that the specified CharSequence would have if + * spaces and control characters were trimmed from the start and end, + * as by {@link String#trim}. + */ + public static int getTrimmedLength(CharSequence s) { + int len = s.length(); + + int start = 0; + while (start < len && s.charAt(start) <= ' ') { + start++; + } + + int end = len; + while (end > start && s.charAt(end - 1) <= ' ') { + end--; + } + + return end - start; + } + + /** + * Returns true if a and b are equal, including if they are both null. + * + * @param a first CharSequence to check + * @param b second CharSequence to check + * @return true if a and b are equal + */ + public static boolean equals(CharSequence a, CharSequence b) { + return a == b || (a != null && a.equals(b)); + } + + // XXX currently this only reverses chars, not spans + public static CharSequence getReverse(CharSequence source, + int start, int end) { + return new Reverser(source, start, end); + } + + private static class Reverser + implements CharSequence, GetChars + { + public Reverser(CharSequence source, int start, int end) { + mSource = source; + mStart = start; + mEnd = end; + } + + public int length() { + return mEnd - mStart; + } + + public CharSequence subSequence(int start, int end) { + char[] buf = new char[end - start]; + + getChars(start, end, buf, 0); + return new String(buf); + } + + public String toString() { + return subSequence(0, length()).toString(); + } + + public char charAt(int off) { + return AndroidCharacter.getMirror(mSource.charAt(mEnd - 1 - off)); + } + + public void getChars(int start, int end, char[] dest, int destoff) { + TextUtils.getChars(mSource, start + mStart, end + mStart, + dest, destoff); + AndroidCharacter.mirror(dest, 0, end - start); + + int len = end - start; + int n = (end - start) / 2; + for (int i = 0; i < n; i++) { + char tmp = dest[destoff + i]; + + dest[destoff + i] = dest[destoff + len - i - 1]; + dest[destoff + len - i - 1] = tmp; + } + } + + private CharSequence mSource; + private int mStart; + private int mEnd; + } + + private static final int ALIGNMENT_SPAN = 1; + private static final int FOREGROUND_COLOR_SPAN = 2; + private static final int RELATIVE_SIZE_SPAN = 3; + private static final int SCALE_X_SPAN = 4; + private static final int STRIKETHROUGH_SPAN = 5; + private static final int UNDERLINE_SPAN = 6; + private static final int STYLE_SPAN = 7; + private static final int BULLET_SPAN = 8; + private static final int QUOTE_SPAN = 9; + private static final int LEADING_MARGIN_SPAN = 10; + private static final int URL_SPAN = 11; + private static final int BACKGROUND_COLOR_SPAN = 12; + private static final int TYPEFACE_SPAN = 13; + private static final int SUPERSCRIPT_SPAN = 14; + private static final int SUBSCRIPT_SPAN = 15; + private static final int ABSOLUTE_SIZE_SPAN = 16; + private static final int TEXT_APPEARANCE_SPAN = 17; + private static final int ANNOTATION = 18; + + /** + * Flatten a CharSequence and whatever styles can be copied across processes + * into the parcel. + */ + public static void writeToParcel(CharSequence cs, Parcel p, + int parcelableFlags) { + if (cs instanceof Spanned) { + p.writeInt(0); + p.writeString(cs.toString()); + + Spanned sp = (Spanned) cs; + Object[] os = sp.getSpans(0, cs.length(), Object.class); + + // note to people adding to this: check more specific types + // before more generic types. also notice that it uses + // "if" instead of "else if" where there are interfaces + // so one object can be several. + + for (int i = 0; i < os.length; i++) { + Object o = os[i]; + Object prop = os[i]; + + if (prop instanceof CharacterStyle) { + prop = ((CharacterStyle) prop).getUnderlying(); + } + + if (prop instanceof AlignmentSpan) { + p.writeInt(ALIGNMENT_SPAN); + p.writeString(((AlignmentSpan) prop).getAlignment().name()); + writeWhere(p, sp, o); + } + + if (prop instanceof ForegroundColorSpan) { + p.writeInt(FOREGROUND_COLOR_SPAN); + p.writeInt(((ForegroundColorSpan) prop).getForegroundColor()); + writeWhere(p, sp, o); + } + + if (prop instanceof RelativeSizeSpan) { + p.writeInt(RELATIVE_SIZE_SPAN); + p.writeFloat(((RelativeSizeSpan) prop).getSizeChange()); + writeWhere(p, sp, o); + } + + if (prop instanceof ScaleXSpan) { + p.writeInt(SCALE_X_SPAN); + p.writeFloat(((ScaleXSpan) prop).getScaleX()); + writeWhere(p, sp, o); + } + + if (prop instanceof StrikethroughSpan) { + p.writeInt(STRIKETHROUGH_SPAN); + writeWhere(p, sp, o); + } + + if (prop instanceof UnderlineSpan) { + p.writeInt(UNDERLINE_SPAN); + writeWhere(p, sp, o); + } + + if (prop instanceof StyleSpan) { + p.writeInt(STYLE_SPAN); + p.writeInt(((StyleSpan) prop).getStyle()); + writeWhere(p, sp, o); + } + + if (prop instanceof LeadingMarginSpan) { + if (prop instanceof BulletSpan) { + p.writeInt(BULLET_SPAN); + writeWhere(p, sp, o); + } else if (prop instanceof QuoteSpan) { + p.writeInt(QUOTE_SPAN); + p.writeInt(((QuoteSpan) prop).getColor()); + writeWhere(p, sp, o); + } else { + p.writeInt(LEADING_MARGIN_SPAN); + p.writeInt(((LeadingMarginSpan) prop). + getLeadingMargin(true)); + p.writeInt(((LeadingMarginSpan) prop). + getLeadingMargin(false)); + writeWhere(p, sp, o); + } + } + + if (prop instanceof URLSpan) { + p.writeInt(URL_SPAN); + p.writeString(((URLSpan) prop).getURL()); + writeWhere(p, sp, o); + } + + if (prop instanceof BackgroundColorSpan) { + p.writeInt(BACKGROUND_COLOR_SPAN); + p.writeInt(((BackgroundColorSpan) prop).getBackgroundColor()); + writeWhere(p, sp, o); + } + + if (prop instanceof TypefaceSpan) { + p.writeInt(TYPEFACE_SPAN); + p.writeString(((TypefaceSpan) prop).getFamily()); + writeWhere(p, sp, o); + } + + if (prop instanceof SuperscriptSpan) { + p.writeInt(SUPERSCRIPT_SPAN); + writeWhere(p, sp, o); + } + + if (prop instanceof SubscriptSpan) { + p.writeInt(SUBSCRIPT_SPAN); + writeWhere(p, sp, o); + } + + if (prop instanceof AbsoluteSizeSpan) { + p.writeInt(ABSOLUTE_SIZE_SPAN); + p.writeInt(((AbsoluteSizeSpan) prop).getSize()); + writeWhere(p, sp, o); + } + + if (prop instanceof TextAppearanceSpan) { + TextAppearanceSpan tas = (TextAppearanceSpan) prop; + p.writeInt(TEXT_APPEARANCE_SPAN); + + String tf = tas.getFamily(); + if (tf != null) { + p.writeInt(1); + p.writeString(tf); + } else { + p.writeInt(0); + } + + p.writeInt(tas.getTextSize()); + p.writeInt(tas.getTextStyle()); + + ColorStateList csl = tas.getTextColor(); + if (csl == null) { + p.writeInt(0); + } else { + p.writeInt(1); + csl.writeToParcel(p, parcelableFlags); + } + + csl = tas.getLinkTextColor(); + if (csl == null) { + p.writeInt(0); + } else { + p.writeInt(1); + csl.writeToParcel(p, parcelableFlags); + } + + writeWhere(p, sp, o); + } + + if (prop instanceof Annotation) { + p.writeInt(ANNOTATION); + p.writeString(((Annotation) prop).getKey()); + p.writeString(((Annotation) prop).getValue()); + writeWhere(p, sp, o); + } + } + + p.writeInt(0); + } else { + p.writeInt(1); + if (cs != null) { + p.writeString(cs.toString()); + } else { + p.writeString(null); + } + } + } + + private static void writeWhere(Parcel p, Spanned sp, Object o) { + p.writeInt(sp.getSpanStart(o)); + p.writeInt(sp.getSpanEnd(o)); + p.writeInt(sp.getSpanFlags(o)); + } + + public static final Parcelable.Creator<CharSequence> CHAR_SEQUENCE_CREATOR + = new Parcelable.Creator<CharSequence>() + { + /** + * Read and return a new CharSequence, possibly with styles, + * from the parcel. + */ + public CharSequence createFromParcel(Parcel p) { + int kind = p.readInt(); + + if (kind == 1) + return p.readString(); + + SpannableString sp = new SpannableString(p.readString()); + + while (true) { + kind = p.readInt(); + + if (kind == 0) + break; + + switch (kind) { + case ALIGNMENT_SPAN: + readSpan(p, sp, new AlignmentSpan.Standard( + Layout.Alignment.valueOf(p.readString()))); + break; + + case FOREGROUND_COLOR_SPAN: + readSpan(p, sp, new ForegroundColorSpan(p.readInt())); + break; + + case RELATIVE_SIZE_SPAN: + readSpan(p, sp, new RelativeSizeSpan(p.readFloat())); + break; + + case SCALE_X_SPAN: + readSpan(p, sp, new ScaleXSpan(p.readFloat())); + break; + + case STRIKETHROUGH_SPAN: + readSpan(p, sp, new StrikethroughSpan()); + break; + + case UNDERLINE_SPAN: + readSpan(p, sp, new UnderlineSpan()); + break; + + case STYLE_SPAN: + readSpan(p, sp, new StyleSpan(p.readInt())); + break; + + case BULLET_SPAN: + readSpan(p, sp, new BulletSpan()); + break; + + case QUOTE_SPAN: + readSpan(p, sp, new QuoteSpan(p.readInt())); + break; + + case LEADING_MARGIN_SPAN: + readSpan(p, sp, new LeadingMarginSpan.Standard(p.readInt(), + p.readInt())); + break; + + case URL_SPAN: + readSpan(p, sp, new URLSpan(p.readString())); + break; + + case BACKGROUND_COLOR_SPAN: + readSpan(p, sp, new BackgroundColorSpan(p.readInt())); + break; + + case TYPEFACE_SPAN: + readSpan(p, sp, new TypefaceSpan(p.readString())); + break; + + case SUPERSCRIPT_SPAN: + readSpan(p, sp, new SuperscriptSpan()); + break; + + case SUBSCRIPT_SPAN: + readSpan(p, sp, new SubscriptSpan()); + break; + + case ABSOLUTE_SIZE_SPAN: + readSpan(p, sp, new AbsoluteSizeSpan(p.readInt())); + break; + + case TEXT_APPEARANCE_SPAN: + readSpan(p, sp, new TextAppearanceSpan( + p.readInt() != 0 + ? p.readString() + : null, + p.readInt(), + p.readInt(), + p.readInt() != 0 + ? ColorStateList.CREATOR.createFromParcel(p) + : null, + p.readInt() != 0 + ? ColorStateList.CREATOR.createFromParcel(p) + : null)); + break; + + case ANNOTATION: + readSpan(p, sp, + new Annotation(p.readString(), p.readString())); + break; + + default: + throw new RuntimeException("bogus span encoding " + kind); + } + } + + return sp; + } + + public CharSequence[] newArray(int size) + { + return new CharSequence[size]; + } + }; + + /** + * Return a new CharSequence in which each of the source strings is + * replaced by the corresponding element of the destinations. + */ + public static CharSequence replace(CharSequence template, + String[] sources, + CharSequence[] destinations) { + SpannableStringBuilder tb = new SpannableStringBuilder(template); + + for (int i = 0; i < sources.length; i++) { + int where = indexOf(tb, sources[i]); + + if (where >= 0) + tb.setSpan(sources[i], where, where + sources[i].length(), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + + for (int i = 0; i < sources.length; i++) { + int start = tb.getSpanStart(sources[i]); + int end = tb.getSpanEnd(sources[i]); + + if (start >= 0) { + tb.replace(start, end, destinations[i]); + } + } + + return tb; + } + + /** + * Replace instances of "^1", "^2", etc. in the + * <code>template</code> CharSequence with the corresponding + * <code>values</code>. "^^" is used to produce a single caret in + * the output. Only up to 9 replacement values are supported, + * "^10" will be produce the first replacement value followed by a + * '0'. + * + * @param template the input text containing "^1"-style + * placeholder values. This object is not modified; a copy is + * returned. + * + * @param values CharSequences substituted into the template. The + * first is substituted for "^1", the second for "^2", and so on. + * + * @return the new CharSequence produced by doing the replacement + * + * @throws IllegalArgumentException if the template requests a + * value that was not provided, or if more than 9 values are + * provided. + */ + public static CharSequence expandTemplate(CharSequence template, + CharSequence... values) { + if (values.length > 9) { + throw new IllegalArgumentException("max of 9 values are supported"); + } + + SpannableStringBuilder ssb = new SpannableStringBuilder(template); + + try { + int i = 0; + while (i < ssb.length()) { + if (ssb.charAt(i) == '^') { + char next = ssb.charAt(i+1); + if (next == '^') { + ssb.delete(i+1, i+2); + ++i; + continue; + } else if (Character.isDigit(next)) { + int which = Character.getNumericValue(next) - 1; + if (which < 0) { + throw new IllegalArgumentException( + "template requests value ^" + (which+1)); + } + if (which >= values.length) { + throw new IllegalArgumentException( + "template requests value ^" + (which+1) + + "; only " + values.length + " provided"); + } + ssb.replace(i, i+2, values[which]); + i += values[which].length(); + continue; + } + } + ++i; + } + } catch (IndexOutOfBoundsException ignore) { + // happens when ^ is the last character in the string. + } + return ssb; + } + + public static int getOffsetBefore(CharSequence text, int offset) { + if (offset == 0) + return 0; + if (offset == 1) + return 0; + + char c = text.charAt(offset - 1); + + if (c >= '\uDC00' && c <= '\uDFFF') { + char c1 = text.charAt(offset - 2); + + if (c1 >= '\uD800' && c1 <= '\uDBFF') + offset -= 2; + else + offset -= 1; + } else { + offset -= 1; + } + + if (text instanceof Spanned) { + ReplacementSpan[] spans = ((Spanned) text).getSpans(offset, offset, + ReplacementSpan.class); + + for (int i = 0; i < spans.length; i++) { + int start = ((Spanned) text).getSpanStart(spans[i]); + int end = ((Spanned) text).getSpanEnd(spans[i]); + + if (start < offset && end > offset) + offset = start; + } + } + + return offset; + } + + public static int getOffsetAfter(CharSequence text, int offset) { + int len = text.length(); + + if (offset == len) + return len; + if (offset == len - 1) + return len; + + char c = text.charAt(offset); + + if (c >= '\uD800' && c <= '\uDBFF') { + char c1 = text.charAt(offset + 1); + + if (c1 >= '\uDC00' && c1 <= '\uDFFF') + offset += 2; + else + offset += 1; + } else { + offset += 1; + } + + if (text instanceof Spanned) { + ReplacementSpan[] spans = ((Spanned) text).getSpans(offset, offset, + ReplacementSpan.class); + + for (int i = 0; i < spans.length; i++) { + int start = ((Spanned) text).getSpanStart(spans[i]); + int end = ((Spanned) text).getSpanEnd(spans[i]); + + if (start < offset && end > offset) + offset = end; + } + } + + return offset; + } + + private static void readSpan(Parcel p, Spannable sp, Object o) { + sp.setSpan(o, p.readInt(), p.readInt(), p.readInt()); + } + + public static void copySpansFrom(Spanned source, int start, int end, + Class kind, + Spannable dest, int destoff) { + if (kind == null) { + kind = Object.class; + } + + Object[] spans = source.getSpans(start, end, kind); + + for (int i = 0; i < spans.length; i++) { + int st = source.getSpanStart(spans[i]); + int en = source.getSpanEnd(spans[i]); + int fl = source.getSpanFlags(spans[i]); + + if (st < start) + st = start; + if (en > end) + en = end; + + dest.setSpan(spans[i], st - start + destoff, en - start + destoff, + fl); + } + } + + public enum TruncateAt { + START, + MIDDLE, + END, + } + + public interface EllipsizeCallback { + /** + * This method is called to report that the specified region of + * text was ellipsized away by a call to {@link #ellipsize}. + */ + public void ellipsized(int start, int end); + } + + private static String sEllipsis = null; + + /** + * Returns the original text if it fits in the specified width + * given the properties of the specified Paint, + * or, if it does not fit, a truncated + * copy with ellipsis character added at the specified edge or center. + */ + public static CharSequence ellipsize(CharSequence text, + TextPaint p, + float avail, TruncateAt where) { + return ellipsize(text, p, avail, where, false, null); + } + + /** + * Returns the original text if it fits in the specified width + * given the properties of the specified Paint, + * or, if it does not fit, a copy with ellipsis character added + * at the specified edge or center. + * If <code>preserveLength</code> is specified, the returned copy + * will be padded with zero-width spaces to preserve the original + * length and offsets instead of truncating. + * If <code>callback</code> is non-null, it will be called to + * report the start and end of the ellipsized range. + */ + public static CharSequence ellipsize(CharSequence text, + TextPaint p, + float avail, TruncateAt where, + boolean preserveLength, + EllipsizeCallback callback) { + if (sEllipsis == null) { + Resources r = Resources.getSystem(); + sEllipsis = r.getString(R.string.ellipsis); + } + + int len = text.length(); + + // Use Paint.breakText() for the non-Spanned case to avoid having + // to allocate memory and accumulate the character widths ourselves. + + if (!(text instanceof Spanned)) { + float wid = p.measureText(text, 0, len); + + if (wid <= avail) { + if (callback != null) { + callback.ellipsized(0, 0); + } + + return text; + } + + float ellipsiswid = p.measureText(sEllipsis); + + if (ellipsiswid > avail) { + if (callback != null) { + callback.ellipsized(0, len); + } + + if (preserveLength) { + char[] buf = obtain(len); + for (int i = 0; i < len; i++) { + buf[i] = '\uFEFF'; + } + String ret = new String(buf, 0, len); + recycle(buf); + return ret; + } else { + return ""; + } + } + + if (where == TruncateAt.START) { + int fit = p.breakText(text, 0, len, false, + avail - ellipsiswid, null); + + if (callback != null) { + callback.ellipsized(0, len - fit); + } + + if (preserveLength) { + return blank(text, 0, len - fit); + } else { + return sEllipsis + text.toString().substring(len - fit, len); + } + } else if (where == TruncateAt.END) { + int fit = p.breakText(text, 0, len, true, + avail - ellipsiswid, null); + + if (callback != null) { + callback.ellipsized(fit, len); + } + + if (preserveLength) { + return blank(text, fit, len); + } else { + return text.toString().substring(0, fit) + sEllipsis; + } + } else /* where == TruncateAt.MIDDLE */ { + int right = p.breakText(text, 0, len, false, + (avail - ellipsiswid) / 2, null); + float used = p.measureText(text, len - right, len); + int left = p.breakText(text, 0, len - right, true, + avail - ellipsiswid - used, null); + + if (callback != null) { + callback.ellipsized(left, len - right); + } + + if (preserveLength) { + return blank(text, left, len - right); + } else { + String s = text.toString(); + return s.substring(0, left) + sEllipsis + + s.substring(len - right, len); + } + } + } + + // But do the Spanned cases by hand, because it's such a pain + // to iterate the span transitions backwards and getTextWidths() + // will give us the information we need. + + // getTextWidths() always writes into the start of the array, + // so measure each span into the first half and then copy the + // results into the second half to use later. + + float[] wid = new float[len * 2]; + TextPaint temppaint = new TextPaint(); + Spanned sp = (Spanned) text; + + int next; + for (int i = 0; i < len; i = next) { + next = sp.nextSpanTransition(i, len, MetricAffectingSpan.class); + + Styled.getTextWidths(p, temppaint, sp, i, next, wid, null); + System.arraycopy(wid, 0, wid, len + i, next - i); + } + + float sum = 0; + for (int i = 0; i < len; i++) { + sum += wid[len + i]; + } + + if (sum <= avail) { + if (callback != null) { + callback.ellipsized(0, 0); + } + + return text; + } + + float ellipsiswid = p.measureText(sEllipsis); + + if (ellipsiswid > avail) { + if (callback != null) { + callback.ellipsized(0, len); + } + + if (preserveLength) { + char[] buf = obtain(len); + for (int i = 0; i < len; i++) { + buf[i] = '\uFEFF'; + } + SpannableString ss = new SpannableString(new String(buf, 0, len)); + recycle(buf); + copySpansFrom(sp, 0, len, Object.class, ss, 0); + return ss; + } else { + return ""; + } + } + + if (where == TruncateAt.START) { + sum = 0; + int i; + + for (i = len; i >= 0; i--) { + float w = wid[len + i - 1]; + + if (w + sum + ellipsiswid > avail) { + break; + } + + sum += w; + } + + if (callback != null) { + callback.ellipsized(0, i); + } + + if (preserveLength) { + SpannableString ss = new SpannableString(blank(text, 0, i)); + copySpansFrom(sp, 0, len, Object.class, ss, 0); + return ss; + } else { + SpannableStringBuilder out = new SpannableStringBuilder(sEllipsis); + out.insert(1, text, i, len); + + return out; + } + } else if (where == TruncateAt.END) { + sum = 0; + int i; + + for (i = 0; i < len; i++) { + float w = wid[len + i]; + + if (w + sum + ellipsiswid > avail) { + break; + } + + sum += w; + } + + if (callback != null) { + callback.ellipsized(i, len); + } + + if (preserveLength) { + SpannableString ss = new SpannableString(blank(text, i, len)); + copySpansFrom(sp, 0, len, Object.class, ss, 0); + return ss; + } else { + SpannableStringBuilder out = new SpannableStringBuilder(sEllipsis); + out.insert(0, text, 0, i); + + return out; + } + } else /* where = TruncateAt.MIDDLE */ { + float lsum = 0, rsum = 0; + int left = 0, right = len; + + float ravail = (avail - ellipsiswid) / 2; + for (right = len; right >= 0; right--) { + float w = wid[len + right - 1]; + + if (w + rsum > ravail) { + break; + } + + rsum += w; + } + + float lavail = avail - ellipsiswid - rsum; + for (left = 0; left < right; left++) { + float w = wid[len + left]; + + if (w + lsum > lavail) { + break; + } + + lsum += w; + } + + if (callback != null) { + callback.ellipsized(left, right); + } + + if (preserveLength) { + SpannableString ss = new SpannableString(blank(text, left, right)); + copySpansFrom(sp, 0, len, Object.class, ss, 0); + return ss; + } else { + SpannableStringBuilder out = new SpannableStringBuilder(sEllipsis); + out.insert(0, text, 0, left); + out.insert(out.length(), text, right, len); + + return out; + } + } + } + + private static String blank(CharSequence source, int start, int end) { + int len = source.length(); + char[] buf = obtain(len); + + if (start != 0) { + getChars(source, 0, start, buf, 0); + } + if (end != len) { + getChars(source, end, len, buf, end); + } + + if (start != end) { + buf[start] = '\u2026'; + + for (int i = start + 1; i < end; i++) { + buf[i] = '\uFEFF'; + } + } + + String ret = new String(buf, 0, len); + recycle(buf); + + return ret; + } + + /** + * Converts a CharSequence of the comma-separated form "Andy, Bob, + * Charles, David" that is too wide to fit into the specified width + * into one like "Andy, Bob, 2 more". + * + * @param text the text to truncate + * @param p the Paint with which to measure the text + * @param avail the horizontal width available for the text + * @param oneMore the string for "1 more" in the current locale + * @param more the string for "%d more" in the current locale + */ + public static CharSequence commaEllipsize(CharSequence text, + TextPaint p, float avail, + String oneMore, + String more) { + int len = text.length(); + char[] buf = new char[len]; + TextUtils.getChars(text, 0, len, buf, 0); + + int commaCount = 0; + for (int i = 0; i < len; i++) { + if (buf[i] == ',') { + commaCount++; + } + } + + float[] wid; + + if (text instanceof Spanned) { + Spanned sp = (Spanned) text; + TextPaint temppaint = new TextPaint(); + wid = new float[len * 2]; + + int next; + for (int i = 0; i < len; i = next) { + next = sp.nextSpanTransition(i, len, MetricAffectingSpan.class); + + Styled.getTextWidths(p, temppaint, sp, i, next, wid, null); + System.arraycopy(wid, 0, wid, len + i, next - i); + } + + System.arraycopy(wid, len, wid, 0, len); + } else { + wid = new float[len]; + p.getTextWidths(text, 0, len, wid); + } + + int ok = 0; + int okRemaining = commaCount + 1; + String okFormat = ""; + + int w = 0; + int count = 0; + + for (int i = 0; i < len; i++) { + w += wid[i]; + + if (buf[i] == ',') { + count++; + + int remaining = commaCount - count + 1; + float moreWid; + String format; + + if (remaining == 1) { + format = " " + oneMore; + } else { + format = " " + String.format(more, remaining); + } + + moreWid = p.measureText(format); + + if (w + moreWid <= avail) { + ok = i + 1; + okRemaining = remaining; + okFormat = format; + } + } + } + + if (w <= avail) { + return text; + } else { + SpannableStringBuilder out = new SpannableStringBuilder(okFormat); + out.insert(0, text, 0, ok); + return out; + } + } + + /* package */ static char[] obtain(int len) { + char[] buf; + + synchronized (sLock) { + buf = sTemp; + sTemp = null; + } + + if (buf == null || buf.length < len) + buf = new char[ArrayUtils.idealCharArraySize(len)]; + + return buf; + } + + /* package */ static void recycle(char[] temp) { + if (temp.length > 1000) + return; + + synchronized (sLock) { + sTemp = temp; + } + } + + /** + * Html-encode the string. + * @param s the string to be encoded + * @return the encoded string + */ + public static String htmlEncode(String s) { + StringBuilder sb = new StringBuilder(); + char c; + for (int i = 0; i < s.length(); i++) { + c = s.charAt(i); + switch (c) { + case '<': + sb.append("<"); //$NON-NLS-1$ + break; + case '>': + sb.append(">"); //$NON-NLS-1$ + break; + case '&': + sb.append("&"); //$NON-NLS-1$ + break; + case '\\': + sb.append("'"); //$NON-NLS-1$ + break; + case '"': + sb.append("""); //$NON-NLS-1$ + break; + default: + sb.append(c); + } + } + return sb.toString(); + } + + /** + * Returns a CharSequence concatenating the specified CharSequences, + * retaining their spans if any. + */ + public static CharSequence concat(CharSequence... text) { + if (text.length == 0) { + return ""; + } + + if (text.length == 1) { + return text[0]; + } + + boolean spanned = false; + for (int i = 0; i < text.length; i++) { + if (text[i] instanceof Spanned) { + spanned = true; + break; + } + } + + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < text.length; i++) { + sb.append(text[i]); + } + + if (!spanned) { + return sb.toString(); + } + + SpannableString ss = new SpannableString(sb); + int off = 0; + for (int i = 0; i < text.length; i++) { + int len = text[i].length(); + + if (text[i] instanceof Spanned) { + copySpansFrom((Spanned) text[i], 0, len, Object.class, ss, off); + } + + off += len; + } + + return new SpannedString(ss); + } + + /** + * Returns whether the given CharSequence contains any printable characters. + */ + public static boolean isGraphic(CharSequence str) { + final int len = str.length(); + for (int i=0; i<len; i++) { + int gc = Character.getType(str.charAt(i)); + if (gc != Character.CONTROL + && gc != Character.FORMAT + && gc != Character.SURROGATE + && gc != Character.UNASSIGNED + && gc != Character.LINE_SEPARATOR + && gc != Character.PARAGRAPH_SEPARATOR + && gc != Character.SPACE_SEPARATOR) { + return true; + } + } + return false; + } + + /** + * Returns whether this character is a printable character. + */ + public static boolean isGraphic(char c) { + int gc = Character.getType(c); + return gc != Character.CONTROL + && gc != Character.FORMAT + && gc != Character.SURROGATE + && gc != Character.UNASSIGNED + && gc != Character.LINE_SEPARATOR + && gc != Character.PARAGRAPH_SEPARATOR + && gc != Character.SPACE_SEPARATOR; + } + + /** + * Returns whether the given CharSequence contains only digits. + */ + public static boolean isDigitsOnly(CharSequence str) { + final int len = str.length(); + for (int i = 0; i < len; i++) { + if (!Character.isDigit(str.charAt(i))) { + return false; + } + } + return true; + } + + private static Object sLock = new Object(); + private static char[] sTemp = null; +} diff --git a/core/java/android/text/TextWatcher.java b/core/java/android/text/TextWatcher.java new file mode 100644 index 0000000..7456b28 --- /dev/null +++ b/core/java/android/text/TextWatcher.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2006 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; + +/** + * When an object of a type is attached to an Editable, its methods will + * be called when the text is changed. + */ +public interface TextWatcher { + /** + * This method is called to notify you that, within <code>s</code>, + * the <code>count</code> characters beginning at <code>start</code> + * are about to be replaced by new text with length <code>after</code>. + * It is an error to attempt to make changes to <code>s</code> from + * this callback. + */ + public void beforeTextChanged(CharSequence s, int start, + int count, int after); + /** + * This method is called to notify you that, within <code>s</code>, + * the <code>count</code> characters beginning at <code>start</code> + * have just replaced old text that had length <code>before</code>. + * It is an error to attempt to make changes to <code>s</code> from + * this callback. + */ + public void onTextChanged(CharSequence s, int start, int before, int count); + + /** + * This method is called to notify you that, somewhere within + * <code>s</code>, the text has been changed. + * It is legitimate to make further changes to <code>s</code> from + * this callback, but be careful not to get yourself into an infinite + * loop, because any changes you make will cause this method to be + * called again recursively. + * (You are not told where the change took place because other + * afterTextChanged() methods may already have made other changes + * and invalidated the offsets. But if you need to know here, + * you can use {@link Spannable#setSpan} in {@link #onTextChanged} + * to mark your place and then look up from here where the span + * ended up. + */ + public void afterTextChanged(Editable s); +} diff --git a/core/java/android/text/method/ArrowKeyMovementMethod.java b/core/java/android/text/method/ArrowKeyMovementMethod.java new file mode 100644 index 0000000..ac2e499 --- /dev/null +++ b/core/java/android/text/method/ArrowKeyMovementMethod.java @@ -0,0 +1,266 @@ +/* + * Copyright (C) 2006 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.method; + +import android.view.KeyEvent; +import android.text.*; +import android.widget.TextView; +import android.view.View; +import android.view.MotionEvent; + +// XXX this doesn't extend MetaKeyKeyListener because the signatures +// don't match. Need to figure that out. Meanwhile the meta keys +// won't work in fields that don't take input. + +public class +ArrowKeyMovementMethod +implements MovementMethod +{ + private boolean up(TextView widget, Spannable buffer) { + boolean cap = MetaKeyKeyListener.getMetaState(buffer, + KeyEvent.META_SHIFT_ON) == 1; + boolean alt = MetaKeyKeyListener.getMetaState(buffer, + KeyEvent.META_ALT_ON) == 1; + Layout layout = widget.getLayout(); + + if (cap) { + if (alt) { + Selection.extendSelection(buffer, 0); + return true; + } else { + return Selection.extendUp(buffer, layout); + } + } else { + if (alt) { + Selection.setSelection(buffer, 0); + return true; + } else { + return Selection.moveUp(buffer, layout); + } + } + } + + private boolean down(TextView widget, Spannable buffer) { + boolean cap = MetaKeyKeyListener.getMetaState(buffer, + KeyEvent.META_SHIFT_ON) == 1; + boolean alt = MetaKeyKeyListener.getMetaState(buffer, + KeyEvent.META_ALT_ON) == 1; + Layout layout = widget.getLayout(); + + if (cap) { + if (alt) { + Selection.extendSelection(buffer, buffer.length()); + return true; + } else { + return Selection.extendDown(buffer, layout); + } + } else { + if (alt) { + Selection.setSelection(buffer, buffer.length()); + return true; + } else { + return Selection.moveDown(buffer, layout); + } + } + } + + private boolean left(TextView widget, Spannable buffer) { + boolean cap = MetaKeyKeyListener.getMetaState(buffer, + KeyEvent.META_SHIFT_ON) == 1; + boolean alt = MetaKeyKeyListener.getMetaState(buffer, + KeyEvent.META_ALT_ON) == 1; + Layout layout = widget.getLayout(); + + if (cap) { + if (alt) { + return Selection.extendToLeftEdge(buffer, layout); + } else { + return Selection.extendLeft(buffer, layout); + } + } else { + if (alt) { + return Selection.moveToLeftEdge(buffer, layout); + } else { + return Selection.moveLeft(buffer, layout); + } + } + } + + private boolean right(TextView widget, Spannable buffer) { + boolean cap = MetaKeyKeyListener.getMetaState(buffer, + KeyEvent.META_SHIFT_ON) == 1; + boolean alt = MetaKeyKeyListener.getMetaState(buffer, + KeyEvent.META_ALT_ON) == 1; + Layout layout = widget.getLayout(); + + if (cap) { + if (alt) { + return Selection.extendToRightEdge(buffer, layout); + } else { + return Selection.extendRight(buffer, layout); + } + } else { + if (alt) { + return Selection.moveToRightEdge(buffer, layout); + } else { + return Selection.moveRight(buffer, layout); + } + } + } + + public boolean onKeyDown(TextView widget, Spannable buffer, int keyCode, KeyEvent event) { + boolean handled = false; + + switch (keyCode) { + case KeyEvent.KEYCODE_DPAD_UP: + handled |= up(widget, buffer); + break; + + case KeyEvent.KEYCODE_DPAD_DOWN: + handled |= down(widget, buffer); + break; + + case KeyEvent.KEYCODE_DPAD_LEFT: + handled |= left(widget, buffer); + break; + + case KeyEvent.KEYCODE_DPAD_RIGHT: + handled |= right(widget, buffer); + break; + } + + if (handled) { + MetaKeyKeyListener.adjustMetaAfterKeypress(buffer); + MetaKeyKeyListener.resetLockedMeta(buffer); + } + + return handled; + } + + public boolean onKeyUp(TextView widget, Spannable buffer, int keyCode, KeyEvent event) { + return false; + } + + public boolean onTrackballEvent(TextView widget, Spannable buffer, + MotionEvent event) { + boolean handled = false; + int x = (int) event.getX(); + int y = (int) event.getY(); + + for (; y < 0; y++) { + handled |= up(widget, buffer); + } + for (; y > 0; y--) { + handled |= down(widget, buffer); + } + + for (; x < 0; x++) { + handled |= left(widget, buffer); + } + for (; x > 0; x--) { + handled |= right(widget, buffer); + } + + if (handled) { + MetaKeyKeyListener.adjustMetaAfterKeypress(buffer); + MetaKeyKeyListener.resetLockedMeta(buffer); + } + + return handled; + } + + public boolean onTouchEvent(TextView widget, Spannable buffer, + MotionEvent event) { + boolean handled = Touch.onTouchEvent(widget, buffer, event); + + if (widget.isFocused()) { + if (event.getAction() == MotionEvent.ACTION_UP) { + int x = (int) event.getX(); + int y = (int) event.getY(); + + x -= widget.getTotalPaddingLeft(); + y -= widget.getTotalPaddingTop(); + + x += widget.getScrollX(); + y += widget.getScrollY(); + + Layout layout = widget.getLayout(); + int line = layout.getLineForVertical(y); + int off = layout.getOffsetForHorizontal(line, x); + + boolean cap = (event.getMetaState() & KeyEvent.META_SHIFT_ON) != 0; + + if (cap) { + Selection.extendSelection(buffer, off); + } else { + Selection.setSelection(buffer, off); + } + + MetaKeyKeyListener.adjustMetaAfterKeypress(buffer); + MetaKeyKeyListener.resetLockedMeta(buffer); + + return true; + } + } + + return handled; + } + + public boolean canSelectArbitrarily() { + return true; + } + + public void initialize(TextView widget, Spannable text) { + Selection.setSelection(text, 0); + } + + public void onTakeFocus(TextView view, Spannable text, int dir) { + if ((dir & (View.FOCUS_FORWARD | View.FOCUS_DOWN)) != 0) { + Layout layout = view.getLayout(); + + if (layout == null) { + /* + * This shouldn't be null, but do something sensible if it is. + */ + Selection.setSelection(text, text.length()); + } else { + /* + * Put the cursor at the end of the first line, which is + * either the last offset if there is only one line, or the + * offset before the first character of the second line + * if there is more than one line. + */ + if (layout.getLineCount() == 1) { + Selection.setSelection(text, text.length()); + } else { + Selection.setSelection(text, layout.getLineStart(1) - 1); + } + } + } else { + Selection.setSelection(text, text.length()); + } + } + + public static MovementMethod getInstance() { + if (sInstance == null) + sInstance = new ArrowKeyMovementMethod(); + + return sInstance; + } + + private static ArrowKeyMovementMethod sInstance; +} diff --git a/core/java/android/text/method/BaseKeyListener.java b/core/java/android/text/method/BaseKeyListener.java new file mode 100644 index 0000000..3e92b7b --- /dev/null +++ b/core/java/android/text/method/BaseKeyListener.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2006 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.method; + +import android.view.KeyEvent; +import android.view.View; +import android.os.Message; +import android.util.Log; +import android.text.*; +import android.widget.TextView; + +public abstract class BaseKeyListener +extends MetaKeyKeyListener +implements KeyListener { + /* package */ static final Object OLD_SEL_START = new Object(); + + /** + * Performs the action that happens when you press the DEL key in + * a TextView. If there is a selection, deletes the selection; + * otherwise, DEL alone deletes the character before the cursor, + * if any; + * ALT+DEL deletes everything on the line the cursor is on. + * + * @return true if anything was deleted; false otherwise. + */ + public boolean backspace(View view, Editable content, int keyCode, + KeyEvent event) { + int selStart, selEnd; + boolean result = true; + + { + int a = Selection.getSelectionStart(content); + int b = Selection.getSelectionEnd(content); + + selStart = Math.min(a, b); + selEnd = Math.max(a, b); + } + + if (selStart != selEnd) { + content.delete(selStart, selEnd); + } else if (altBackspace(view, content, keyCode, event)) { + result = true; + } else { + int to = TextUtils.getOffsetBefore(content, selEnd); + + if (to != selEnd) { + content.delete(Math.min(to, selEnd), Math.max(to, selEnd)); + } + else { + result = false; + } + } + + if (result) + adjustMetaAfterKeypress(content); + + return result; + } + + private boolean altBackspace(View view, Editable content, int keyCode, + KeyEvent event) { + if (getMetaState(content, META_ALT_ON) != 1) { + return false; + } + + if (!(view instanceof TextView)) { + return false; + } + + Layout layout = ((TextView) view).getLayout(); + + if (layout == null) { + return false; + } + + int l = layout.getLineForOffset(Selection.getSelectionStart(content)); + int start = layout.getLineStart(l); + int end = layout.getLineEnd(l); + + if (end == start) { + return false; + } + + content.delete(start, end); + return true; + } + + public boolean onKeyDown(View view, Editable content, + int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_DEL) { + backspace(view, content, keyCode, event); + return true; + } + + return super.onKeyDown(view, content, keyCode, event); + } +} + diff --git a/core/java/android/text/method/CharacterPickerDialog.java b/core/java/android/text/method/CharacterPickerDialog.java new file mode 100644 index 0000000..d787132 --- /dev/null +++ b/core/java/android/text/method/CharacterPickerDialog.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2008 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.method; + +import com.android.internal.R; + +import android.app.Dialog; +import android.content.Context; +import android.os.Bundle; +import android.text.*; +import android.view.LayoutInflater; +import android.view.View.OnClickListener; +import android.view.View; +import android.view.ViewGroup.LayoutParams; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.AdapterView; +import android.widget.BaseAdapter; +import android.widget.Button; +import android.widget.GridView; +import android.widget.TextView; + +/** + * Dialog for choosing accented characters related to a base character. + */ +public class CharacterPickerDialog extends Dialog + implements OnItemClickListener, OnClickListener { + private View mView; + private Editable mText; + private String mOptions; + private boolean mInsert; + private LayoutInflater mInflater; + + /** + * Creates a new CharacterPickerDialog that presents the specified + * <code>options</code> for insertion or replacement (depending on + * the sense of <code>insert</code>) into <code>text</code>. + */ + public CharacterPickerDialog(Context context, View view, + Editable text, String options, + boolean insert) { + super(context); + + mView = view; + mText = text; + mOptions = options; + mInsert = insert; + mInflater = LayoutInflater.from(context); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + WindowManager.LayoutParams params = getWindow().getAttributes(); + params.token = mView.getApplicationWindowToken(); + params.type = params.TYPE_APPLICATION_PANEL; + + setTitle(R.string.select_character); + setContentView(R.layout.character_picker); + + GridView grid = (GridView) findViewById(R.id.characterPicker); + grid.setAdapter(new OptionsAdapter(getContext())); + grid.setOnItemClickListener(this); + + findViewById(R.id.cancel).setOnClickListener(this); + } + + /** + * Handles clicks on the character buttons. + */ + public void onItemClick(AdapterView parent, View view, int position, long id) { + int selEnd = Selection.getSelectionEnd(mText); + String result = String.valueOf(mOptions.charAt(position)); + + if (mInsert || selEnd == 0) { + mText.insert(selEnd, result); + } else { + mText.replace(selEnd - 1, selEnd, result); + } + + dismiss(); + } + + /** + * Handles clicks on the Cancel button. + */ + public void onClick(View v) { + dismiss(); + } + + private class OptionsAdapter extends BaseAdapter { + private Context mContext; + + public OptionsAdapter(Context context) { + super(); + mContext = context; + } + + public View getView(int position, View convertView, ViewGroup parent) { + Button b = (Button) + mInflater.inflate(R.layout.character_picker_button, null); + b.setText(String.valueOf(mOptions.charAt(position))); + return b; + } + + public final int getCount() { + return mOptions.length(); + } + + public final Object getItem(int position) { + return String.valueOf(mOptions.charAt(position)); + } + + public final long getItemId(int position) { + return position; + } + } +} diff --git a/core/java/android/text/method/DateKeyListener.java b/core/java/android/text/method/DateKeyListener.java new file mode 100644 index 0000000..0ca0332 --- /dev/null +++ b/core/java/android/text/method/DateKeyListener.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2006 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.method; + +import android.view.KeyEvent; + +/** + * For entering dates in a text field. + */ +public class DateKeyListener extends NumberKeyListener +{ + @Override + protected char[] getAcceptedChars() + { + return CHARACTERS; + } + + public static DateKeyListener getInstance() { + if (sInstance != null) + return sInstance; + + sInstance = new DateKeyListener(); + return sInstance; + } + + /** + * The characters that are used. + * + * @see KeyEvent#getMatch + * @see #getAcceptedChars + */ + public static final char[] CHARACTERS = new char[] { + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + '/', '-', '.' + }; + + private static DateKeyListener sInstance; +} diff --git a/core/java/android/text/method/DateTimeKeyListener.java b/core/java/android/text/method/DateTimeKeyListener.java new file mode 100644 index 0000000..304d326 --- /dev/null +++ b/core/java/android/text/method/DateTimeKeyListener.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2006 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.method; + +import android.view.KeyEvent; + +/** + * For entering dates and times in the same text field. + */ +public class DateTimeKeyListener extends NumberKeyListener +{ + @Override + protected char[] getAcceptedChars() + { + return CHARACTERS; + } + + public static DateTimeKeyListener getInstance() { + if (sInstance != null) + return sInstance; + + sInstance = new DateTimeKeyListener(); + return sInstance; + } + + /** + * The characters that are used. + * + * @see KeyEvent#getMatch + * @see #getAcceptedChars + */ + public static final char[] CHARACTERS = new char[] { + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'm', + 'p', ':', '/', '-', ' ' + }; + + private static DateTimeKeyListener sInstance; +} diff --git a/core/java/android/text/method/DialerKeyListener.java b/core/java/android/text/method/DialerKeyListener.java new file mode 100644 index 0000000..e805ad7 --- /dev/null +++ b/core/java/android/text/method/DialerKeyListener.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2006 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.method; + +import android.view.KeyEvent; +import android.view.KeyCharacterMap.KeyData; +import android.util.SparseIntArray; +import android.text.Spannable; + +/** + * For dialing-only text entry + */ +public class DialerKeyListener extends NumberKeyListener +{ + @Override + protected char[] getAcceptedChars() + { + return CHARACTERS; + } + + public static DialerKeyListener getInstance() { + if (sInstance != null) + return sInstance; + + sInstance = new DialerKeyListener(); + return sInstance; + } + + /** + * Overrides the superclass's lookup method to prefer the number field + * from the KeyEvent. + */ + protected int lookup(KeyEvent event, Spannable content) { + int meta = getMetaState(content); + int number = event.getNumber(); + + /* + * Prefer number if no meta key is active, or if it produces something + * valid and the meta lookup does not. + */ + if ((meta & (KeyEvent.META_ALT_ON | KeyEvent.META_SHIFT_ON)) == 0) { + if (number != 0) { + return number; + } + } + + int match = super.lookup(event, content); + + if (match != 0) { + return match; + } else { + /* + * If a meta key is active but the lookup with the meta key + * did not produce anything, try some other meta keys, because + * the user might have pressed SHIFT when they meant ALT, + * or vice versa. + */ + + if (meta != 0) { + KeyData kd = new KeyData(); + char[] accepted = getAcceptedChars(); + + if (event.getKeyData(kd)) { + for (int i = 1; i < kd.meta.length; i++) { + if (ok(accepted, kd.meta[i])) { + return kd.meta[i]; + } + } + } + } + + /* + * Otherwise, use the number associated with the key, since + * whatever they wanted to do with the meta key does not + * seem to be valid here. + */ + + return number; + } + } + + + /** + * The characters that are used. + * + * @see KeyEvent#getMatch + * @see #getAcceptedChars + */ + public static final char[] CHARACTERS = new char[] { + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '#', '*', + '+', '-', '(', ')', ',', '/', 'N', '.', ' ' + }; + + private static DialerKeyListener sInstance; +} diff --git a/core/java/android/text/method/DigitsKeyListener.java b/core/java/android/text/method/DigitsKeyListener.java new file mode 100644 index 0000000..99a3f1a --- /dev/null +++ b/core/java/android/text/method/DigitsKeyListener.java @@ -0,0 +1,206 @@ +/* + * Copyright (C) 2006 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.method; + +import android.text.Spanned; +import android.text.SpannableStringBuilder; +import android.view.KeyEvent; + + +/** + * For digits-only text entry + */ +public class DigitsKeyListener extends NumberKeyListener +{ + private char[] mAccepted; + private boolean mSign; + private boolean mDecimal; + + private static final int SIGN = 1; + private static final int DECIMAL = 2; + + @Override + protected char[] getAcceptedChars() { + return mAccepted; + } + + /** + * The characters that are used. + * + * @see KeyEvent#getMatch + * @see #getAcceptedChars + */ + private static final char[][] CHARACTERS = new char[][] { + new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' }, + new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-' }, + new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.' }, + new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '.' }, + }; + + /** + * Allocates a DigitsKeyListener that accepts the digits 0 through 9. + */ + public DigitsKeyListener() { + this(false, false); + } + + /** + * Allocates a DigitsKeyListener that accepts the digits 0 through 9, + * plus the minus sign (only at the beginning) and/or decimal point + * (only one per field) if specified. + */ + public DigitsKeyListener(boolean sign, boolean decimal) { + mSign = sign; + mDecimal = decimal; + + int kind = (sign ? SIGN : 0) | (decimal ? DECIMAL : 0); + mAccepted = CHARACTERS[kind]; + } + + /** + * Returns a DigitsKeyListener that accepts the digits 0 through 9. + */ + public static DigitsKeyListener getInstance() { + return getInstance(false, false); + } + + /** + * Returns a DigitsKeyListener that accepts the digits 0 through 9, + * plus the minus sign (only at the beginning) and/or decimal point + * (only one per field) if specified. + */ + public static DigitsKeyListener getInstance(boolean sign, boolean decimal) { + int kind = (sign ? SIGN : 0) | (decimal ? DECIMAL : 0); + + if (sInstance[kind] != null) + return sInstance[kind]; + + sInstance[kind] = new DigitsKeyListener(sign, decimal); + return sInstance[kind]; + } + + /** + * Returns a DigitsKeyListener that accepts only the characters + * that appear in the specified String. Note that not all characters + * may be available on every keyboard. + */ + public static DigitsKeyListener getInstance(String accepted) { + // TODO: do we need a cache of these to avoid allocating? + + DigitsKeyListener dim = new DigitsKeyListener(); + + dim.mAccepted = new char[accepted.length()]; + accepted.getChars(0, accepted.length(), dim.mAccepted, 0); + + return dim; + } + + @Override + public CharSequence filter(CharSequence source, int start, int end, + Spanned dest, int dstart, int dend) { + CharSequence out = super.filter(source, start, end, dest, dstart, dend); + + if (mSign == false && mDecimal == false) { + return out; + } + + if (out != null) { + source = out; + start = 0; + end = out.length(); + } + + int sign = -1; + int decimal = -1; + int dlen = dest.length(); + + /* + * Find out if the existing text has '-' or '.' characters. + */ + + for (int i = 0; i < dstart; i++) { + char c = dest.charAt(i); + + if (c == '-') { + sign = i; + } else if (c == '.') { + decimal = i; + } + } + for (int i = dend; i < dlen; i++) { + char c = dest.charAt(i); + + if (c == '-') { + return ""; // Nothing can be inserted in front of a '-'. + } else if (c == '.') { + decimal = i; + } + } + + /* + * If it does, we must strip them out from the source. + * In addition, '-' must be the very first character, + * and nothing can be inserted before an existing '-'. + * Go in reverse order so the offsets are stable. + */ + + SpannableStringBuilder stripped = null; + + for (int i = end - 1; i >= start; i--) { + char c = source.charAt(i); + boolean strip = false; + + if (c == '-') { + if (i != start || dstart != 0) { + strip = true; + } else if (sign >= 0) { + strip = true; + } else { + sign = i; + } + } else if (c == '.') { + if (decimal >= 0) { + strip = true; + } else { + decimal = i; + } + } + + if (strip) { + if (end == start + 1) { + return ""; // Only one character, and it was stripped. + } + + if (stripped == null) { + stripped = new SpannableStringBuilder(source, start, end); + } + + stripped.delete(i - start, i + 1 - start); + } + } + + if (stripped != null) { + return stripped; + } else if (out != null) { + return out; + } else { + return null; + } + } + + private static DigitsKeyListener[] sInstance = new DigitsKeyListener[4]; +} diff --git a/core/java/android/text/method/HideReturnsTransformationMethod.java b/core/java/android/text/method/HideReturnsTransformationMethod.java new file mode 100644 index 0000000..ce18692 --- /dev/null +++ b/core/java/android/text/method/HideReturnsTransformationMethod.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2006 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.method; + +import android.graphics.Rect; +import android.text.GetChars; +import android.text.Spanned; +import android.text.SpannedString; +import android.text.TextUtils; +import android.view.View; + +/** + * This transformation method causes any carriage return characters (\r) + * to be hidden by displaying them as zero-width non-breaking space + * characters (\uFEFF). + */ +public class HideReturnsTransformationMethod +extends ReplacementTransformationMethod { + private static char[] ORIGINAL = new char[] { '\r' }; + private static char[] REPLACEMENT = new char[] { '\uFEFF' }; + + /** + * The character to be replaced is \r. + */ + protected char[] getOriginal() { + return ORIGINAL; + } + + /** + * The character that \r is replaced with is \uFEFF. + */ + protected char[] getReplacement() { + return REPLACEMENT; + } + + public static HideReturnsTransformationMethod getInstance() { + if (sInstance != null) + return sInstance; + + sInstance = new HideReturnsTransformationMethod(); + return sInstance; + } + + private static HideReturnsTransformationMethod sInstance; +} diff --git a/core/java/android/text/method/KeyListener.java b/core/java/android/text/method/KeyListener.java new file mode 100644 index 0000000..05ab72d --- /dev/null +++ b/core/java/android/text/method/KeyListener.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2006 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.method; + +import android.view.KeyEvent; +import android.view.View; +import android.os.Message; +import android.text.*; +import android.widget.TextView; + +public interface KeyListener +{ + /** + * If the key listener wants to handle this key, return true, + * otherwise return false and the caller (i.e. the widget host) + * will handle the key. + */ + public boolean onKeyDown(View view, Editable text, + int keyCode, KeyEvent event); + + /** + * If the key listener wants to handle this key release, return true, + * otherwise return false and the caller (i.e. the widget host) + * will handle the key. + */ + public boolean onKeyUp(View view, Editable text, + int keyCode, KeyEvent event); +} diff --git a/core/java/android/text/method/LinkMovementMethod.java b/core/java/android/text/method/LinkMovementMethod.java new file mode 100644 index 0000000..92ac531 --- /dev/null +++ b/core/java/android/text/method/LinkMovementMethod.java @@ -0,0 +1,256 @@ +/* + * Copyright (C) 2006 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.method; + +import android.content.Intent; +import android.net.Uri; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.text.*; +import android.text.style.*; +import android.view.View; +import android.widget.TextView; + +public class +LinkMovementMethod +extends ScrollingMovementMethod +{ + private static final int CLICK = 1; + private static final int UP = 2; + private static final int DOWN = 3; + + @Override + public boolean onKeyDown(TextView widget, Spannable buffer, + int keyCode, KeyEvent event) { + switch (keyCode) { + case KeyEvent.KEYCODE_DPAD_CENTER: + case KeyEvent.KEYCODE_ENTER: + if (event.getRepeatCount() == 0) { + if (action(CLICK, widget, buffer)) { + return true; + } + } + } + + return super.onKeyDown(widget, buffer, keyCode, event); + } + + @Override + protected boolean up(TextView widget, Spannable buffer) { + if (action(UP, widget, buffer)) { + return true; + } + + return super.up(widget, buffer); + } + + @Override + protected boolean down(TextView widget, Spannable buffer) { + if (action(DOWN, widget, buffer)) { + return true; + } + + return super.down(widget, buffer); + } + + @Override + protected boolean left(TextView widget, Spannable buffer) { + if (action(UP, widget, buffer)) { + return true; + } + + return super.left(widget, buffer); + } + + @Override + protected boolean right(TextView widget, Spannable buffer) { + if (action(DOWN, widget, buffer)) { + return true; + } + + return super.right(widget, buffer); + } + + private boolean action(int what, TextView widget, Spannable buffer) { + boolean handled = false; + + Layout layout = widget.getLayout(); + + int padding = widget.getTotalPaddingTop() + + widget.getTotalPaddingBottom(); + int areatop = widget.getScrollY(); + int areabot = areatop + widget.getHeight() - padding; + + int linetop = layout.getLineForVertical(areatop); + int linebot = layout.getLineForVertical(areabot); + + int first = layout.getLineStart(linetop); + int last = layout.getLineEnd(linebot); + + ClickableSpan[] candidates = buffer.getSpans(first, last, ClickableSpan.class); + + int a = Selection.getSelectionStart(buffer); + int b = Selection.getSelectionEnd(buffer); + + int selStart = Math.min(a, b); + int selEnd = Math.max(a, b); + + if (selStart < 0) { + if (buffer.getSpanStart(FROM_BELOW) >= 0) { + selStart = selEnd = buffer.length(); + } + } + + if (selStart > last) + selStart = selEnd = Integer.MAX_VALUE; + if (selEnd < first) + selStart = selEnd = -1; + + switch (what) { + case CLICK: + if (selStart == selEnd) { + return false; + } + + ClickableSpan[] link = buffer.getSpans(selStart, selEnd, ClickableSpan.class); + + if (link.length != 1) + return false; + + link[0].onClick(widget); + break; + + case UP: + int beststart, bestend; + + beststart = -1; + bestend = -1; + + for (int i = 0; i < candidates.length; i++) { + int end = buffer.getSpanEnd(candidates[i]); + + if (end < selEnd || selStart == selEnd) { + if (end > bestend) { + beststart = buffer.getSpanStart(candidates[i]); + bestend = end; + } + } + } + + if (beststart >= 0) { + Selection.setSelection(buffer, bestend, beststart); + return true; + } + + break; + + case DOWN: + beststart = Integer.MAX_VALUE; + bestend = Integer.MAX_VALUE; + + for (int i = 0; i < candidates.length; i++) { + int start = buffer.getSpanStart(candidates[i]); + + if (start > selStart || selStart == selEnd) { + if (start < beststart) { + beststart = start; + bestend = buffer.getSpanEnd(candidates[i]); + } + } + } + + if (bestend < Integer.MAX_VALUE) { + Selection.setSelection(buffer, beststart, bestend); + return true; + } + + break; + } + + return false; + } + + public boolean onKeyUp(TextView widget, Spannable buffer, + int keyCode, KeyEvent event) { + return false; + } + + @Override + public boolean onTouchEvent(TextView widget, Spannable buffer, + MotionEvent event) { + int action = event.getAction(); + + if (action == MotionEvent.ACTION_UP || + action == MotionEvent.ACTION_DOWN) { + int x = (int) event.getX(); + int y = (int) event.getY(); + + x -= widget.getTotalPaddingLeft(); + y -= widget.getTotalPaddingTop(); + + x += widget.getScrollX(); + y += widget.getScrollY(); + + Layout layout = widget.getLayout(); + int line = layout.getLineForVertical(y); + int off = layout.getOffsetForHorizontal(line, x); + + ClickableSpan[] link = buffer.getSpans(off, off, ClickableSpan.class); + + if (link.length != 0) { + if (action == MotionEvent.ACTION_UP) { + link[0].onClick(widget); + } else if (action == MotionEvent.ACTION_DOWN) { + Selection.setSelection(buffer, + buffer.getSpanStart(link[0]), + buffer.getSpanEnd(link[0])); + } + + return true; + } else { + Selection.removeSelection(buffer); + } + } + + return super.onTouchEvent(widget, buffer, event); + } + + public void initialize(TextView widget, Spannable text) { + Selection.removeSelection(text); + text.removeSpan(FROM_BELOW); + } + + public void onTakeFocus(TextView view, Spannable text, int dir) { + Selection.removeSelection(text); + + if ((dir & View.FOCUS_BACKWARD) != 0) { + text.setSpan(FROM_BELOW, 0, 0, Spannable.SPAN_POINT_POINT); + } else { + text.removeSpan(FROM_BELOW); + } + } + + public static MovementMethod getInstance() { + if (sInstance == null) + sInstance = new LinkMovementMethod(); + + return sInstance; + } + + private static LinkMovementMethod sInstance; + private static Object FROM_BELOW = new Object(); +} diff --git a/core/java/android/text/method/MetaKeyKeyListener.java b/core/java/android/text/method/MetaKeyKeyListener.java new file mode 100644 index 0000000..2d75b87 --- /dev/null +++ b/core/java/android/text/method/MetaKeyKeyListener.java @@ -0,0 +1,250 @@ +/* + * Copyright (C) 2006 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.method; + +import android.view.KeyEvent; +import android.view.View; +import android.text.*; + +/** + * This base class encapsulates the behavior for handling the meta keys + * (caps, fn, sym). Key listener that care about meta state should + * inherit from it; you should not instantiate this class directly in a client. + */ + +public abstract class MetaKeyKeyListener { + public static final int META_SHIFT_ON = KeyEvent.META_SHIFT_ON; + public static final int META_ALT_ON = KeyEvent.META_ALT_ON; + public static final int META_SYM_ON = KeyEvent.META_SYM_ON; + + public static final int META_CAP_LOCKED = KeyEvent.META_SHIFT_ON << 8; + public static final int META_ALT_LOCKED = KeyEvent.META_ALT_ON << 8; + public static final int META_SYM_LOCKED = KeyEvent.META_SYM_ON << 8; + + private static final Object CAP = new Object(); + private static final Object ALT = new Object(); + private static final Object SYM = new Object(); + + /** + * Resets all meta state to inactive. + */ + public static void resetMetaState(Spannable text) { + text.removeSpan(CAP); + text.removeSpan(ALT); + text.removeSpan(SYM); + } + + /** + * Gets the state of the meta keys. + * + * @param text the buffer in which the meta key would have been pressed. + * + * @return an integer in which each bit set to one represents a pressed + * or locked meta key. + */ + public static final int getMetaState(CharSequence text) { + return getActive(text, CAP, META_SHIFT_ON, META_CAP_LOCKED) | + getActive(text, ALT, META_ALT_ON, META_ALT_LOCKED) | + getActive(text, SYM, META_SYM_ON, META_SYM_LOCKED); + } + + /** + * Gets the state of a particular meta key. + * + * @param meta META_SHIFT_ON, META_ALT_ON, or META_SYM_ON + * @param text the buffer in which the meta key would have been pressed. + * + * @return 0 if inactive, 1 if active, 2 if locked. + */ + public static final int getMetaState(CharSequence text, int meta) { + switch (meta) { + case META_SHIFT_ON: + return getActive(text, CAP, 1, 2); + + case META_ALT_ON: + return getActive(text, ALT, 1, 2); + + case META_SYM_ON: + return getActive(text, SYM, 1, 2); + + default: + return 0; + } + } + + private static int getActive(CharSequence text, Object meta, + int on, int lock) { + if (!(text instanceof Spanned)) { + return 0; + } + + Spanned sp = (Spanned) text; + int flag = sp.getSpanFlags(meta); + + if (flag == LOCKED) { + return lock; + } else if (flag != 0) { + return on; + } else { + return 0; + } + } + + /** + * Call this method after you handle a keypress so that the meta + * state will be reset to unshifted (if it is not still down) + * or primed to be reset to unshifted (once it is released). + */ + public static void adjustMetaAfterKeypress(Spannable content) { + adjust(content, CAP); + adjust(content, ALT); + adjust(content, SYM); + } + + /** + * Returns true if this object is one that this class would use to + * keep track of meta state in the specified text. + */ + public static boolean isMetaTracker(CharSequence text, Object what) { + return what == CAP || what == ALT || what == SYM; + } + + private static void adjust(Spannable content, Object what) { + int current = content.getSpanFlags(what); + + if (current == PRESSED) + content.setSpan(what, 0, 0, USED); + else if (current == RELEASED) + content.removeSpan(what); + } + + /** + * Call this if you are a method that ignores the locked meta state + * (arrow keys, for example) and you handle a key. + */ + protected static void resetLockedMeta(Spannable content) { + resetLock(content, CAP); + resetLock(content, ALT); + resetLock(content, SYM); + } + + private static void resetLock(Spannable content, Object what) { + int current = content.getSpanFlags(what); + + if (current == LOCKED) + content.removeSpan(what); + } + + /** + * Handles presses of the meta keys. + */ + public boolean onKeyDown(View view, Editable content, + int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_SHIFT_LEFT || keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT) { + press(content, CAP); + return true; + } + + if (keyCode == KeyEvent.KEYCODE_ALT_LEFT || keyCode == KeyEvent.KEYCODE_ALT_RIGHT + || keyCode == KeyEvent.KEYCODE_NUM) { + press(content, ALT); + return true; + } + + if (keyCode == KeyEvent.KEYCODE_SYM) { + press(content, SYM); + return true; + } + + return false; // no super to call through to + } + + private void press(Editable content, Object what) { + int state = content.getSpanFlags(what); + + if (state == PRESSED) + ; // repeat before use + else if (state == RELEASED) + content.setSpan(what, 0, 0, LOCKED); + else if (state == USED) + ; // repeat after use + else if (state == LOCKED) + content.removeSpan(what); + else + content.setSpan(what, 0, 0, PRESSED); + } + + /** + * Handles release of the meta keys. + */ + public boolean onKeyUp(View view, Editable content, int keyCode, + KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_SHIFT_LEFT || keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT) { + release(content, CAP); + return true; + } + + if (keyCode == KeyEvent.KEYCODE_ALT_LEFT || keyCode == KeyEvent.KEYCODE_ALT_RIGHT + || keyCode == KeyEvent.KEYCODE_NUM) { + release(content, ALT); + return true; + } + + if (keyCode == KeyEvent.KEYCODE_SYM) { + release(content, SYM); + return true; + } + + return false; // no super to call through to + } + + private void release(Editable content, Object what) { + int current = content.getSpanFlags(what); + + if (current == USED) + content.removeSpan(what); + else if (current == PRESSED) + content.setSpan(what, 0, 0, RELEASED); + } + + /** + * The meta key has been pressed but has not yet been used. + */ + private static final int PRESSED = + Spannable.SPAN_MARK_MARK | (1 << Spannable.SPAN_USER_SHIFT); + + /** + * The meta key has been pressed and released but has still + * not yet been used. + */ + private static final int RELEASED = + Spannable.SPAN_MARK_MARK | (2 << Spannable.SPAN_USER_SHIFT); + + /** + * The meta key has been pressed and used but has not yet been released. + */ + private static final int USED = + Spannable.SPAN_MARK_MARK | (3 << Spannable.SPAN_USER_SHIFT); + + /** + * The meta key has been pressed and released without use, and then + * pressed again; it may also have been released again. + */ + private static final int LOCKED = + Spannable.SPAN_MARK_MARK | (4 << Spannable.SPAN_USER_SHIFT); +} + diff --git a/core/java/android/text/method/MovementMethod.java b/core/java/android/text/method/MovementMethod.java new file mode 100644 index 0000000..9e37e59 --- /dev/null +++ b/core/java/android/text/method/MovementMethod.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2006 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.method; + +import android.widget.TextView; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.text.*; + +public interface MovementMethod +{ + public void initialize(TextView widget, Spannable text); + public boolean onKeyDown(TextView widget, Spannable text, int keyCode, KeyEvent event); + public boolean onKeyUp(TextView widget, Spannable text, int keyCode, KeyEvent event); + public void onTakeFocus(TextView widget, Spannable text, int direction); + public boolean onTrackballEvent(TextView widget, Spannable text, + MotionEvent event); + public boolean onTouchEvent(TextView widget, Spannable text, + MotionEvent event); + + /** + * Returns true if this movement method allows arbitrary selection + * of any text; false if it has no selection (like a movement method + * that only scrolls) or a constrained selection (for example + * limited to links. The "Select All" menu item is disabled + * if arbitrary selection is not allowed. + */ + public boolean canSelectArbitrarily(); +} diff --git a/core/java/android/text/method/MultiTapKeyListener.java b/core/java/android/text/method/MultiTapKeyListener.java new file mode 100644 index 0000000..7137d40 --- /dev/null +++ b/core/java/android/text/method/MultiTapKeyListener.java @@ -0,0 +1,285 @@ +/* + * Copyright (C) 2006 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.method; + +import android.view.KeyEvent; +import android.view.View; +import android.os.Message; +import android.os.Handler; +import android.os.SystemClock; +import android.text.*; +import android.text.method.TextKeyListener.Capitalize; +import android.widget.TextView; +import android.util.SparseArray; +import android.util.SparseIntArray; + +/** + * This is the standard key listener for alphabetic input on 12-key + * keyboards. You should generally not need to instantiate this yourself; + * TextKeyListener will do it for you. + */ +public class MultiTapKeyListener extends BaseKeyListener + implements SpanWatcher { + private static MultiTapKeyListener[] sInstance = + new MultiTapKeyListener[Capitalize.values().length * 2]; + + private static final SparseArray<String> sRecs = new SparseArray<String>(); + + private Capitalize mCapitalize; + private boolean mAutoText; + + static { + sRecs.put(KeyEvent.KEYCODE_1, ".,1!@#$%^&*:/?'=()"); + sRecs.put(KeyEvent.KEYCODE_2, "abc2ABC"); + sRecs.put(KeyEvent.KEYCODE_3, "def3DEF"); + sRecs.put(KeyEvent.KEYCODE_4, "ghi4GHI"); + sRecs.put(KeyEvent.KEYCODE_5, "jkl5JKL"); + sRecs.put(KeyEvent.KEYCODE_6, "mno6MNO"); + sRecs.put(KeyEvent.KEYCODE_7, "pqrs7PQRS"); + sRecs.put(KeyEvent.KEYCODE_8, "tuv8TUV"); + sRecs.put(KeyEvent.KEYCODE_9, "wxyz9WXYZ"); + sRecs.put(KeyEvent.KEYCODE_0, "0+"); + sRecs.put(KeyEvent.KEYCODE_POUND, " "); + }; + + public MultiTapKeyListener(Capitalize cap, + boolean autotext) { + mCapitalize = cap; + mAutoText = autotext; + } + + /** + * Returns a new or existing instance with the specified capitalization + * and correction properties. + */ + public static MultiTapKeyListener getInstance(boolean autotext, + Capitalize cap) { + int off = cap.ordinal() * 2 + (autotext ? 1 : 0); + + if (sInstance[off] == null) { + sInstance[off] = new MultiTapKeyListener(cap, autotext); + } + + return sInstance[off]; + } + + public boolean onKeyDown(View view, Editable content, + int keyCode, KeyEvent event) { + int selStart, selEnd; + int pref = 0; + + if (view != null) { + pref = TextKeyListener.getInstance().getPrefs(view.getContext()); + } + + { + int a = Selection.getSelectionStart(content); + int b = Selection.getSelectionEnd(content); + + selStart = Math.min(a, b); + selEnd = Math.max(a, b); + } + + int activeStart = content.getSpanStart(TextKeyListener.ACTIVE); + int activeEnd = content.getSpanEnd(TextKeyListener.ACTIVE); + + // now for the multitap cases... + + // Try to increment the character we were working on before + // if we have one and it's still the same key. + + int rec = (content.getSpanFlags(TextKeyListener.ACTIVE) + & Spannable.SPAN_USER) >>> Spannable.SPAN_USER_SHIFT; + + if (activeStart == selStart && activeEnd == selEnd && + selEnd - selStart == 1 && + rec >= 0 && rec < sRecs.size()) { + if (keyCode == KeyEvent.KEYCODE_STAR) { + char current = content.charAt(selStart); + + if (Character.isLowerCase(current)) { + content.replace(selStart, selEnd, + String.valueOf(current).toUpperCase()); + removeTimeouts(content); + Timeout t = new Timeout(content); + + return true; + } + if (Character.isUpperCase(current)) { + content.replace(selStart, selEnd, + String.valueOf(current).toLowerCase()); + removeTimeouts(content); + Timeout t = new Timeout(content); + + return true; + } + } + + if (sRecs.indexOfKey(keyCode) == rec) { + String val = sRecs.valueAt(rec); + char ch = content.charAt(selStart); + int ix = val.indexOf(ch); + + if (ix >= 0) { + ix = (ix + 1) % (val.length()); + + content.replace(selStart, selEnd, val, ix, ix + 1); + removeTimeouts(content); + Timeout t = new Timeout(content); + + return true; + } + } + + // Is this key one we know about at all? If so, acknowledge + // that the selection is our fault but the key has changed + // or the text no longer matches, so move the selection over + // so that it inserts instead of replaces. + + rec = sRecs.indexOfKey(keyCode); + + if (rec >= 0) { + Selection.setSelection(content, selEnd, selEnd); + selStart = selEnd; + } + } else { + rec = sRecs.indexOfKey(keyCode); + } + + if (rec >= 0) { + // We have a valid key. Replace the selection or insertion point + // with the first character for that key, and remember what + // record it came from for next time. + + String val = sRecs.valueAt(rec); + + int off = 0; + if ((pref & TextKeyListener.AUTO_CAP) != 0 && + TextKeyListener.shouldCap(mCapitalize, content, selStart)) { + for (int i = 0; i < val.length(); i++) { + if (Character.isUpperCase(val.charAt(i))) { + off = i; + break; + } + } + } + + if (selStart != selEnd) { + Selection.setSelection(content, selEnd); + } + + content.setSpan(OLD_SEL_START, selStart, selStart, + Spannable.SPAN_MARK_MARK); + + content.replace(selStart, selEnd, val, off, off + 1); + + int oldStart = content.getSpanStart(OLD_SEL_START); + selEnd = Selection.getSelectionEnd(content); + + if (selEnd != oldStart) { + Selection.setSelection(content, oldStart, selEnd); + + content.setSpan(TextKeyListener.LAST_TYPED, + oldStart, selEnd, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + + content.setSpan(TextKeyListener.ACTIVE, + oldStart, selEnd, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE | + (rec << Spannable.SPAN_USER_SHIFT)); + + } + + removeTimeouts(content); + Timeout t = new Timeout(content); + + // Set up the callback so we can remove the timeout if the + // cursor moves. + + if (content.getSpanStart(this) < 0) { + KeyListener[] methods = content.getSpans(0, content.length(), + KeyListener.class); + for (Object method : methods) { + content.removeSpan(method); + } + content.setSpan(this, 0, content.length(), + Spannable.SPAN_INCLUSIVE_INCLUSIVE); + } + + return true; + } + + return super.onKeyDown(view, content, keyCode, event); + } + + public void onSpanChanged(Spannable buf, + Object what, int s, int e, int start, int stop) { + if (what == Selection.SELECTION_END) { + buf.removeSpan(TextKeyListener.ACTIVE); + removeTimeouts(buf); + } + } + + private static void removeTimeouts(Spannable buf) { + Timeout[] timeout = buf.getSpans(0, buf.length(), Timeout.class); + + for (int i = 0; i < timeout.length; i++) { + Timeout t = timeout[i]; + + t.removeCallbacks(t); + t.mBuffer = null; + buf.removeSpan(t); + } + } + + private class Timeout + extends Handler + implements Runnable + { + public Timeout(Editable buffer) { + mBuffer = buffer; + mBuffer.setSpan(Timeout.this, 0, mBuffer.length(), + Spannable.SPAN_INCLUSIVE_INCLUSIVE); + + postAtTime(this, SystemClock.uptimeMillis() + 2000); + } + + public void run() { + Spannable buf = mBuffer; + + if (buf != null) { + int st = Selection.getSelectionStart(buf); + int en = Selection.getSelectionEnd(buf); + + int start = buf.getSpanStart(TextKeyListener.ACTIVE); + int end = buf.getSpanEnd(TextKeyListener.ACTIVE); + + if (st == start && en == end) { + Selection.setSelection(buf, Selection.getSelectionEnd(buf)); + } + + buf.removeSpan(Timeout.this); + } + } + + private Editable mBuffer; + } + + public void onSpanAdded(Spannable s, Object what, int start, int end) { } + public void onSpanRemoved(Spannable s, Object what, int start, int end) { } +} + diff --git a/core/java/android/text/method/NumberKeyListener.java b/core/java/android/text/method/NumberKeyListener.java new file mode 100644 index 0000000..348b658 --- /dev/null +++ b/core/java/android/text/method/NumberKeyListener.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2006 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.method; + +import android.view.KeyEvent; +import android.view.View; +import android.text.Editable; +import android.text.InputFilter; +import android.text.Selection; +import android.text.Spannable; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.util.SparseIntArray; + +/** + * For numeric text entry + */ +public abstract class NumberKeyListener extends BaseKeyListener + implements InputFilter +{ + /** + * You can say which characters you can accept. + */ + protected abstract char[] getAcceptedChars(); + + protected int lookup(KeyEvent event, Spannable content) { + return event.getMatch(getAcceptedChars(), getMetaState(content)); + } + + public CharSequence filter(CharSequence source, int start, int end, + Spanned dest, int dstart, int dend) { + char[] accept = getAcceptedChars(); + boolean filter = false; + + int i; + for (i = start; i < end; i++) { + if (!ok(accept, source.charAt(i))) { + break; + } + } + + if (i == end) { + // It was all OK. + return null; + } + + if (end - start == 1) { + // It was not OK, and there is only one char, so nothing remains. + return ""; + } + + SpannableStringBuilder filtered = + new SpannableStringBuilder(source, start, end); + i -= start; + end -= start; + + int len = end - start; + // Only count down to i because the chars before that were all OK. + for (int j = end - 1; j >= i; j--) { + if (!ok(accept, source.charAt(j))) { + filtered.delete(j, j + 1); + } + } + + return filtered; + } + + protected static boolean ok(char[] accept, char c) { + for (int i = accept.length - 1; i >= 0; i--) { + if (accept[i] == c) { + return true; + } + } + + return false; + } + + @Override + public boolean onKeyDown(View view, Editable content, + int keyCode, KeyEvent event) { + int selStart, selEnd; + + { + int a = Selection.getSelectionStart(content); + int b = Selection.getSelectionEnd(content); + + selStart = Math.min(a, b); + selEnd = Math.max(a, b); + } + + int i = event != null ? lookup(event, content) : 0; + int repeatCount = event != null ? event.getRepeatCount() : 0; + if (repeatCount == 0) { + if (i != 0) { + if (selStart != selEnd) { + Selection.setSelection(content, selEnd); + } + + content.replace(selStart, selEnd, String.valueOf((char) i)); + + adjustMetaAfterKeypress(content); + return true; + } + } else if (i == '0' && repeatCount == 1) { + // Pretty hackish, it replaces the 0 with the + + + if (selStart == selEnd && selEnd > 0 && + content.charAt(selStart - 1) == '0') { + content.replace(selStart - 1, selEnd, String.valueOf('+')); + adjustMetaAfterKeypress(content); + return true; + } + } + + adjustMetaAfterKeypress(content); + return super.onKeyDown(view, content, keyCode, event); + } +} diff --git a/core/java/android/text/method/PasswordTransformationMethod.java b/core/java/android/text/method/PasswordTransformationMethod.java new file mode 100644 index 0000000..edaa836 --- /dev/null +++ b/core/java/android/text/method/PasswordTransformationMethod.java @@ -0,0 +1,260 @@ +/* + * Copyright (C) 2006 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.method; + +import android.os.Handler; +import android.os.SystemClock; +import android.graphics.Rect; +import android.view.View; +import android.text.Editable; +import android.text.GetChars; +import android.text.TextUtils; +import android.text.TextWatcher; +import android.text.Selection; +import android.text.Spanned; +import android.text.Spannable; +import android.text.style.UpdateLayout; + +import java.lang.ref.WeakReference; + +public class PasswordTransformationMethod +implements TransformationMethod, TextWatcher +{ + public CharSequence getTransformation(CharSequence source, View view) { + if (source instanceof Spannable) { + Spannable sp = (Spannable) source; + + /* + * Remove any references to other views that may still be + * attached. This will happen when you flip the screen + * while a password field is showing; there will still + * be references to the old EditText in the text. + */ + ViewReference[] vr = sp.getSpans(0, sp.length(), + ViewReference.class); + for (int i = 0; i < vr.length; i++) { + sp.removeSpan(vr[i]); + } + + sp.setSpan(new ViewReference(view), 0, 0, + Spannable.SPAN_POINT_POINT); + } + + return new PasswordCharSequence(source); + } + + public static PasswordTransformationMethod getInstance() { + if (sInstance != null) + return sInstance; + + sInstance = new PasswordTransformationMethod(); + return sInstance; + } + + public void beforeTextChanged(CharSequence s, int start, + int count, int after) { + // This callback isn't used. + } + + public void onTextChanged(CharSequence s, int start, + int before, int count) { + if (s instanceof Spannable) { + Spannable sp = (Spannable) s; + ViewReference[] vr = sp.getSpans(0, s.length(), + ViewReference.class); + if (vr.length == 0) { + return; + } + + /* + * There should generally only be one ViewReference in the text, + * but make sure to look through all of them if necessary in case + * something strange is going on. (We might still end up with + * multiple ViewReferences if someone moves text from one password + * field to another.) + */ + View v = null; + for (int i = 0; v == null && i < vr.length; i++) { + v = vr[i].get(); + } + + if (v == null) { + return; + } + + int pref = TextKeyListener.getInstance().getPrefs(v.getContext()); + if ((pref & TextKeyListener.SHOW_PASSWORD) != 0) { + if (count > 0) { + Visible[] old = sp.getSpans(0, sp.length(), Visible.class); + for (int i = 0; i < old.length; i++) { + sp.removeSpan(old[i]); + } + + sp.setSpan(new Visible(sp, this), start, start + count, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + } + } + + public void afterTextChanged(Editable s) { + // This callback isn't used. + } + + public void onFocusChanged(View view, CharSequence sourceText, + boolean focused, int direction, + Rect previouslyFocusedRect) { + if (!focused) { + if (sourceText instanceof Spannable) { + Spannable sp = (Spannable) sourceText; + + Visible[] old = sp.getSpans(0, sp.length(), Visible.class); + for (int i = 0; i < old.length; i++) { + sp.removeSpan(old[i]); + } + } + } + } + + private static class PasswordCharSequence + implements CharSequence, GetChars + { + public PasswordCharSequence(CharSequence source) { + mSource = source; + } + + public int length() { + return mSource.length(); + } + + public char charAt(int i) { + if (mSource instanceof Spanned) { + Spanned sp = (Spanned) mSource; + + int st = sp.getSpanStart(TextKeyListener.ACTIVE); + int en = sp.getSpanEnd(TextKeyListener.ACTIVE); + + if (i >= st && i < en) { + return mSource.charAt(i); + } + + Visible[] visible = sp.getSpans(0, sp.length(), Visible.class); + + for (int a = 0; a < visible.length; a++) { + if (sp.getSpanStart(visible[a].mTransformer) >= 0) { + st = sp.getSpanStart(visible[a]); + en = sp.getSpanEnd(visible[a]); + + if (i >= st && i < en) { + return mSource.charAt(i); + } + } + } + } + + return DOT; + } + + public CharSequence subSequence(int start, int end) { + char[] buf = new char[end - start]; + + getChars(start, end, buf, 0); + return new String(buf); + } + + public String toString() { + return subSequence(0, length()).toString(); + } + + public void getChars(int start, int end, char[] dest, int off) { + TextUtils.getChars(mSource, start, end, dest, off); + + int st = -1, en = -1; + int nvisible = 0; + int[] starts = null, ends = null; + + if (mSource instanceof Spanned) { + Spanned sp = (Spanned) mSource; + + st = sp.getSpanStart(TextKeyListener.ACTIVE); + en = sp.getSpanEnd(TextKeyListener.ACTIVE); + + Visible[] visible = sp.getSpans(0, sp.length(), Visible.class); + nvisible = visible.length; + starts = new int[nvisible]; + ends = new int[nvisible]; + + for (int i = 0; i < nvisible; i++) { + if (sp.getSpanStart(visible[i].mTransformer) >= 0) { + starts[i] = sp.getSpanStart(visible[i]); + ends[i] = sp.getSpanEnd(visible[i]); + } + } + } + + for (int i = start; i < end; i++) { + if (! (i >= st && i < en)) { + boolean visible = false; + + for (int a = 0; a < nvisible; a++) { + if (i >= starts[a] && i < ends[a]) { + visible = true; + break; + } + } + + if (!visible) { + dest[i - start + off] = DOT; + } + } + } + } + + private CharSequence mSource; + } + + private static class Visible + extends Handler + implements UpdateLayout, Runnable + { + public Visible(Spannable sp, PasswordTransformationMethod ptm) { + mText = sp; + mTransformer = ptm; + postAtTime(this, SystemClock.uptimeMillis() + 1500); + } + + public void run() { + mText.removeSpan(this); + } + + private Spannable mText; + private PasswordTransformationMethod mTransformer; + } + + /** + * Used to stash a reference back to the View in the Editable so we + * can use it to check the settings. + */ + private static class ViewReference extends WeakReference<View> { + public ViewReference(View v) { + super(v); + } + } + + private static PasswordTransformationMethod sInstance; + private static char DOT = '\u2022'; +} diff --git a/core/java/android/text/method/QwertyKeyListener.java b/core/java/android/text/method/QwertyKeyListener.java new file mode 100644 index 0000000..ae7ba8f --- /dev/null +++ b/core/java/android/text/method/QwertyKeyListener.java @@ -0,0 +1,444 @@ +/* + * Copyright (C) 2006 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.method; + +import android.os.Message; +import android.os.Handler; +import android.text.*; +import android.text.method.TextKeyListener.Capitalize; +import android.util.SparseArray; +import android.util.SparseIntArray; +import android.view.KeyCharacterMap; +import android.view.KeyEvent; +import android.view.View; +import android.widget.TextView; + +import java.util.HashMap; + +/** + * This is the standard key listener for alphabetic input on qwerty + * keyboards. You should generally not need to instantiate this yourself; + * TextKeyListener will do it for you. + */ +public class QwertyKeyListener extends BaseKeyListener { + private static QwertyKeyListener[] sInstance = + new QwertyKeyListener[Capitalize.values().length * 2]; + + public QwertyKeyListener(Capitalize cap, boolean autotext) { + mAutoCap = cap; + mAutoText = autotext; + } + + /** + * Returns a new or existing instance with the specified capitalization + * and correction properties. + */ + public static QwertyKeyListener getInstance(boolean autotext, + Capitalize cap) { + int off = cap.ordinal() * 2 + (autotext ? 1 : 0); + + if (sInstance[off] == null) { + sInstance[off] = new QwertyKeyListener(cap, autotext); + } + + return sInstance[off]; + } + + public boolean onKeyDown(View view, Editable content, + int keyCode, KeyEvent event) { + int selStart, selEnd; + int pref = 0; + + if (view != null) { + pref = TextKeyListener.getInstance().getPrefs(view.getContext()); + } + + { + int a = Selection.getSelectionStart(content); + int b = Selection.getSelectionEnd(content); + + selStart = Math.min(a, b); + selEnd = Math.max(a, b); + + if (selStart < 0 || selEnd < 0) { + selStart = selEnd = 0; + Selection.setSelection(content, 0, 0); + } + } + + int activeStart = content.getSpanStart(TextKeyListener.ACTIVE); + int activeEnd = content.getSpanEnd(TextKeyListener.ACTIVE); + + // QWERTY keyboard normal case + + int i = event.getUnicodeChar(getMetaState(content)); + + int count = event.getRepeatCount(); + if (count > 0 && selStart == selEnd && selStart > 0) { + char c = content.charAt(selStart - 1); + + if (c == i || c == Character.toUpperCase(i) && view != null) { + if (showCharacterPicker(view, content, c, false, count)) { + resetMetaState(content); + return true; + } + } + } + + if (i == KeyCharacterMap.PICKER_DIALOG_INPUT) { + if (view != null) { + showCharacterPicker(view, content, + KeyCharacterMap.PICKER_DIALOG_INPUT, true, 1); + } + resetMetaState(content); + return true; + } + + if (i == KeyCharacterMap.HEX_INPUT) { + int start; + + if (selStart == selEnd) { + start = selEnd; + + while (start > 0 && selEnd - start < 4 && + Character.digit(content.charAt(start - 1), 16) >= 0) { + start--; + } + } else { + start = selStart; + } + + int ch = -1; + try { + String hex = TextUtils.substring(content, start, selEnd); + ch = Integer.parseInt(hex, 16); + } catch (NumberFormatException nfe) { } + + if (ch >= 0) { + selStart = start; + Selection.setSelection(content, selStart, selEnd); + i = ch; + } else { + i = 0; + } + } + + if (i != 0) { + boolean dead = false; + + if ((i & KeyCharacterMap.COMBINING_ACCENT) != 0) { + dead = true; + i = i & KeyCharacterMap.COMBINING_ACCENT_MASK; + } + + if (activeStart == selStart && activeEnd == selEnd) { + boolean replace = false; + + if (selEnd - selStart - 1 == 0) { + char accent = content.charAt(selStart); + int composed = event.getDeadChar(accent, i); + + if (composed != 0) { + i = composed; + replace = true; + } + } + + if (!replace) { + Selection.setSelection(content, selEnd); + content.removeSpan(TextKeyListener.ACTIVE); + selStart = selEnd; + } + } + + if ((pref & TextKeyListener.AUTO_CAP) != 0 && + Character.isLowerCase(i) && + TextKeyListener.shouldCap(mAutoCap, content, selStart)) { + int where = content.getSpanEnd(TextKeyListener.CAPPED); + int flags = content.getSpanFlags(TextKeyListener.CAPPED); + + if (where == selStart && (((flags >> 16) & 0xFFFF) == i)) { + content.removeSpan(TextKeyListener.CAPPED); + } else { + flags = i << 16; + i = Character.toUpperCase(i); + + if (selStart == 0) + content.setSpan(TextKeyListener.CAPPED, 0, 0, + Spannable.SPAN_MARK_MARK | flags); + else + content.setSpan(TextKeyListener.CAPPED, + selStart - 1, selStart, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE | + flags); + } + } + + if (selStart != selEnd) { + Selection.setSelection(content, selEnd); + } + content.setSpan(OLD_SEL_START, selStart, selStart, + Spannable.SPAN_MARK_MARK); + + content.replace(selStart, selEnd, String.valueOf((char) i)); + + int oldStart = content.getSpanStart(OLD_SEL_START); + selEnd = Selection.getSelectionEnd(content); + + if (oldStart < selEnd) { + content.setSpan(TextKeyListener.LAST_TYPED, + oldStart, selEnd, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + + if (dead) { + Selection.setSelection(content, oldStart, selEnd); + content.setSpan(TextKeyListener.ACTIVE, oldStart, selEnd, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + + adjustMetaAfterKeypress(content); + + // potentially do autotext replacement if the character + // that was typed was an autotext terminator + + if ((pref & TextKeyListener.AUTO_TEXT) != 0 && mAutoText && + (i == ' ' || i == '\t' || i == '\n' || + i == ',' || i == '.' || i == '!' || i == '?' || + i == '"' || i == ')' || i == ']') && + content.getSpanEnd(TextKeyListener.INHIBIT_REPLACEMENT) + != oldStart) { + int x; + + for (x = oldStart; x > 0; x--) { + char c = content.charAt(x - 1); + if (c != '\'' && !Character.isLetter(c)) { + break; + } + } + + String rep = getReplacement(content, x, oldStart, view); + + if (rep != null) { + Replaced[] repl = content.getSpans(0, content.length(), + Replaced.class); + for (int a = 0; a < repl.length; a++) + content.removeSpan(repl[a]); + + char[] orig = new char[oldStart - x]; + TextUtils.getChars(content, x, oldStart, orig, 0); + + content.setSpan(new Replaced(orig), x, oldStart, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + content.replace(x, oldStart, rep); + } + } + + // Replace two spaces by a period and a space. + + if ((pref & TextKeyListener.AUTO_PERIOD) != 0 && mAutoText) { + selEnd = Selection.getSelectionEnd(content); + if (selEnd - 3 >= 0) { + if (content.charAt(selEnd - 1) == ' ' && + content.charAt(selEnd - 2) == ' ') { + char c = content.charAt(selEnd - 3); + + if (Character.isLetter(c)) { + content.replace(selEnd - 2, selEnd - 1, "."); + } + } + } + } + + return true; + } else if (keyCode == KeyEvent.KEYCODE_DEL && selStart == selEnd) { + // special backspace case for undoing autotext + + int consider = 1; + + // if backspacing over the last typed character, + // it undoes the autotext prior to that character + // (unless the character typed was newline, in which + // case this behavior would be confusing) + + if (content.getSpanEnd(TextKeyListener.LAST_TYPED) == selStart) { + if (content.charAt(selStart - 1) != '\n') + consider = 2; + } + + Replaced[] repl = content.getSpans(selStart - consider, selStart, + Replaced.class); + + if (repl.length > 0) { + int st = content.getSpanStart(repl[0]); + int en = content.getSpanEnd(repl[0]); + String old = new String(repl[0].mText); + + content.removeSpan(repl[0]); + content.setSpan(TextKeyListener.INHIBIT_REPLACEMENT, + en, en, Spannable.SPAN_POINT_POINT); + content.replace(st, en, old); + + en = content.getSpanStart(TextKeyListener.INHIBIT_REPLACEMENT); + if (en - 1 >= 0) { + content.setSpan(TextKeyListener.INHIBIT_REPLACEMENT, + en - 1, en, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } else { + content.removeSpan(TextKeyListener.INHIBIT_REPLACEMENT); + } + + adjustMetaAfterKeypress(content); + + return true; + } + } + + return super.onKeyDown(view, content, keyCode, event); + } + + private String getReplacement(CharSequence src, int start, int end, + View view) { + int len = end - start; + boolean changecase = false; + + String replacement = AutoText.get(src, start, end, view); + + if (replacement == null) { + String key = TextUtils.substring(src, start, end).toLowerCase(); + replacement = AutoText.get(key, 0, end - start, view); + changecase = true; + + if (replacement == null) + return null; + } + + int caps = 0; + + if (changecase) { + for (int j = start; j < end; j++) { + if (Character.isUpperCase(src.charAt(j))) + caps++; + } + } + + String out; + + if (caps == 0) + out = replacement; + else if (caps == 1) + out = toTitleCase(replacement); + else if (caps == len) + out = replacement.toUpperCase(); + else + out = toTitleCase(replacement); + + if (out.length() == len && + TextUtils.regionMatches(src, start, out, 0, len)) + return null; + + return out; + } + + /** + * Marks the specified region of <code>content</code> as having + * contained <code>original</code> prior to AutoText replacement. + * Call this method when you have done or are about to do an + * AutoText-style replacement on a region of text and want to let + * the same mechanism (the user pressing DEL immediately after the + * change) undo the replacement. + * + * @param content the Editable text where the replacement was made + * @param start the start of the replaced region + * @param end the end of the replaced region; the location of the cursor + * @param original the text to be restored if the user presses DEL + */ + public static void markAsReplaced(Spannable content, int start, int end, + String original) { + Replaced[] repl = content.getSpans(0, content.length(), Replaced.class); + for (int a = 0; a < repl.length; a++) { + content.removeSpan(repl[a]); + } + + int len = original.length(); + char[] orig = new char[len]; + original.getChars(0, len, orig, 0); + + content.setSpan(new Replaced(orig), start, end, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + + private static SparseArray<String> PICKER_SETS = + new SparseArray<String>(); + static { + PICKER_SETS.put('!', "\u00A1"); + PICKER_SETS.put('<', "\u00AB"); + PICKER_SETS.put('>', "\u00BB"); + PICKER_SETS.put('?', "\u00BF"); + PICKER_SETS.put('A', "\u00C0\u00C1\u00C2\u00C4\u00C6\u00C3\u00C5"); + PICKER_SETS.put('C', "\u00C7"); + PICKER_SETS.put('E', "\u00C8\u00C9\u00CA\u00CB"); + PICKER_SETS.put('I', "\u00CC\u00CD\u00CE\u00CF"); + PICKER_SETS.put('N', "\u00D1"); + PICKER_SETS.put('O', "\u00D8\u0152\u00D5\u00D2\u00D3\u00D4\u00D6"); + PICKER_SETS.put('U', "\u00D9\u00DA\u00DB\u00DC"); + PICKER_SETS.put('Y', "\u00DD\u0178"); + PICKER_SETS.put('a', "\u00E0\u00E1\u00E2\u00E4\u00E6\u00E3\u00E5"); + PICKER_SETS.put('c', "\u00E7"); + PICKER_SETS.put('e', "\u00E8\u00E9\u00EA\u00EB"); + PICKER_SETS.put('i', "\u00EC\u00ED\u00EE\u00EF"); + PICKER_SETS.put('n', "\u00F1"); + PICKER_SETS.put('o', "\u00F8\u0153\u00F5\u00F2\u00F3\u00F4\u00F6"); + PICKER_SETS.put('s', "\u00A7\u00DF"); + PICKER_SETS.put('u', "\u00F9\u00FA\u00FB\u00FC"); + PICKER_SETS.put('y', "\u00FD\u00FF"); + PICKER_SETS.put(KeyCharacterMap.PICKER_DIALOG_INPUT, + "\u2026\u00A5\u2022\u00AE\u00A9\u00B1"); + }; + + private boolean showCharacterPicker(View view, Editable content, char c, + boolean insert, int count) { + String set = PICKER_SETS.get(c); + if (set == null) { + return false; + } + + if (count == 1) { + new CharacterPickerDialog(view.getContext(), + view, content, set, insert).show(); + } + + return true; + } + + private static String toTitleCase(String src) { + return Character.toUpperCase(src.charAt(0)) + src.substring(1); + } + + /* package */ static class Replaced + { + public Replaced(char[] text) { + mText = text; + } + + private char[] mText; + } + + private Capitalize mAutoCap; + private boolean mAutoText; +} + diff --git a/core/java/android/text/method/ReplacementTransformationMethod.java b/core/java/android/text/method/ReplacementTransformationMethod.java new file mode 100644 index 0000000..d6f879a --- /dev/null +++ b/core/java/android/text/method/ReplacementTransformationMethod.java @@ -0,0 +1,205 @@ +/* + * Copyright (C) 2006 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.method; + +import android.graphics.Rect; +import android.text.Editable; +import android.text.GetChars; +import android.text.Spannable; +import android.text.Spanned; +import android.text.SpannedString; +import android.text.TextUtils; +import android.view.View; + +/** + * This transformation method causes the characters in the {@link #getOriginal} + * array to be replaced by the corresponding characters in the + * {@link #getReplacement} array. + */ +public abstract class ReplacementTransformationMethod +implements TransformationMethod +{ + /** + * Returns the list of characters that are to be replaced by other + * characters when displayed. + */ + protected abstract char[] getOriginal(); + /** + * Returns a parallel array of replacement characters for the ones + * that are to be replaced. + */ + protected abstract char[] getReplacement(); + + /** + * Returns a CharSequence that will mirror the contents of the + * source CharSequence but with the characters in {@link #getOriginal} + * replaced by ones from {@link #getReplacement}. + */ + public CharSequence getTransformation(CharSequence source, View v) { + char[] original = getOriginal(); + char[] replacement = getReplacement(); + + /* + * Short circuit for faster display if the text will never change. + */ + if (!(source instanceof Editable)) { + /* + * Check whether the text does not contain any of the + * source characters so can be used unchanged. + */ + boolean doNothing = true; + int n = original.length; + for (int i = 0; i < n; i++) { + if (TextUtils.indexOf(source, original[i]) >= 0) { + doNothing = false; + break; + } + } + if (doNothing) { + return source; + } + + if (!(source instanceof Spannable)) { + /* + * The text contains some of the source characters, + * but they can be flattened out now instead of + * at display time. + */ + if (source instanceof Spanned) { + return new SpannedString(new SpannedReplacementCharSequence( + (Spanned) source, + original, replacement)); + } else { + return new ReplacementCharSequence(source, + original, + replacement).toString(); + } + } + } + + if (source instanceof Spanned) { + return new SpannedReplacementCharSequence((Spanned) source, + original, replacement); + } else { + return new ReplacementCharSequence(source, original, replacement); + } + } + + public void onFocusChanged(View view, CharSequence sourceText, + boolean focused, int direction, + Rect previouslyFocusedRect) { + // This callback isn't used. + } + + private static class ReplacementCharSequence + implements CharSequence, GetChars { + private char[] mOriginal, mReplacement; + + public ReplacementCharSequence(CharSequence source, char[] original, + char[] replacement) { + mSource = source; + mOriginal = original; + mReplacement = replacement; + } + + public int length() { + return mSource.length(); + } + + public char charAt(int i) { + char c = mSource.charAt(i); + + int n = mOriginal.length; + for (int j = 0; j < n; j++) { + if (c == mOriginal[j]) { + c = mReplacement[j]; + } + } + + return c; + } + + public CharSequence subSequence(int start, int end) { + char[] c = new char[end - start]; + + getChars(start, end, c, 0); + return new String(c); + } + + public String toString() { + char[] c = new char[length()]; + + getChars(0, length(), c, 0); + return new String(c); + } + + public void getChars(int start, int end, char[] dest, int off) { + TextUtils.getChars(mSource, start, end, dest, off); + int offend = end - start + off; + int n = mOriginal.length; + + for (int i = off; i < offend; i++) { + char c = dest[i]; + + for (int j = 0; j < n; j++) { + if (c == mOriginal[j]) { + dest[i] = mReplacement[j]; + } + } + } + } + + private CharSequence mSource; + } + + private static class SpannedReplacementCharSequence + extends ReplacementCharSequence + implements Spanned + { + public SpannedReplacementCharSequence(Spanned source, char[] original, + char[] replacement) { + super(source, original, replacement); + mSpanned = source; + } + + public CharSequence subSequence(int start, int end) { + return new SpannedString(this).subSequence(start, end); + } + + public <T> T[] getSpans(int start, int end, Class<T> type) { + return mSpanned.getSpans(start, end, type); + } + + public int getSpanStart(Object tag) { + return mSpanned.getSpanStart(tag); + } + + public int getSpanEnd(Object tag) { + return mSpanned.getSpanEnd(tag); + } + + public int getSpanFlags(Object tag) { + return mSpanned.getSpanFlags(tag); + } + + public int nextSpanTransition(int start, int end, Class type) { + return mSpanned.nextSpanTransition(start, end, type); + } + + private Spanned mSpanned; + } +} diff --git a/core/java/android/text/method/ScrollingMovementMethod.java b/core/java/android/text/method/ScrollingMovementMethod.java new file mode 100644 index 0000000..0438e1e --- /dev/null +++ b/core/java/android/text/method/ScrollingMovementMethod.java @@ -0,0 +1,234 @@ +/* + * Copyright (C) 2006 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.method; + +import android.util.Log; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.text.*; +import android.widget.TextView; +import android.view.View; + +public class +ScrollingMovementMethod +implements MovementMethod +{ + /** + * Scrolls the text to the left if possible. + */ + protected boolean left(TextView widget, Spannable buffer) { + Layout layout = widget.getLayout(); + + int scrolly = widget.getScrollY(); + int scr = widget.getScrollX(); + int em = Math.round(layout.getPaint().getFontSpacing()); + + int padding = widget.getTotalPaddingTop() + + widget.getTotalPaddingBottom(); + int top = layout.getLineForVertical(scrolly); + int bottom = layout.getLineForVertical(scrolly + widget.getHeight() - + padding); + int left = Integer.MAX_VALUE; + + for (int i = top; i <= bottom; i++) { + left = (int) Math.min(left, layout.getLineLeft(i)); + } + + if (scr > left) { + int s = Math.max(scr - em, left); + widget.scrollTo(s, widget.getScrollY()); + return true; + } + + return false; + } + + /** + * Scrolls the text to the right if possible. + */ + protected boolean right(TextView widget, Spannable buffer) { + Layout layout = widget.getLayout(); + + int scrolly = widget.getScrollY(); + int scr = widget.getScrollX(); + int em = Math.round(layout.getPaint().getFontSpacing()); + + int padding = widget.getTotalPaddingTop() + + widget.getTotalPaddingBottom(); + int top = layout.getLineForVertical(scrolly); + int bottom = layout.getLineForVertical(scrolly + widget.getHeight() - + padding); + int right = 0; + + for (int i = top; i <= bottom; i++) { + right = (int) Math.max(right, layout.getLineRight(i)); + } + + padding = widget.getTotalPaddingLeft() + widget.getTotalPaddingRight(); + if (scr < right - (widget.getWidth() - padding)) { + int s = Math.min(scr + em, right - (widget.getWidth() - padding)); + widget.scrollTo(s, widget.getScrollY()); + return true; + } + + return false; + } + + /** + * Scrolls the text up if possible. + */ + protected boolean up(TextView widget, Spannable buffer) { + Layout layout = widget.getLayout(); + + int areatop = widget.getScrollY(); + int line = layout.getLineForVertical(areatop); + int linetop = layout.getLineTop(line); + + // If the top line is partially visible, bring it all the way + // into view; otherwise, bring the previous line into view. + if (areatop == linetop) + line--; + + if (line >= 0) { + Touch.scrollTo(widget, layout, + widget.getScrollX(), layout.getLineTop(line)); + return true; + } + + return false; + } + + /** + * Scrolls the text down if possible. + */ + protected boolean down(TextView widget, Spannable buffer) { + Layout layout = widget.getLayout(); + + int padding = widget.getTotalPaddingTop() + + widget.getTotalPaddingBottom(); + + int areabot = widget.getScrollY() + widget.getHeight() - padding; + int line = layout.getLineForVertical(areabot); + + if (layout.getLineTop(line+1) < areabot + 1) { + // Less than a pixel of this line is out of view, + // so we must have tried to make it entirely in view + // and now want the next line to be in view instead. + + line++; + } + + if (line <= layout.getLineCount() - 1) { + widget.scrollTo(widget.getScrollX(), layout.getLineTop(line+1) - + (widget.getHeight() - padding)); + Touch.scrollTo(widget, layout, + widget.getScrollX(), widget.getScrollY()); + return true; + } + + return false; + } + + public boolean onKeyDown(TextView widget, Spannable buffer, int keyCode, KeyEvent event) { + boolean handled = false; + + switch (keyCode) { + case KeyEvent.KEYCODE_DPAD_LEFT: + handled |= left(widget, buffer); + break; + + case KeyEvent.KEYCODE_DPAD_RIGHT: + handled |= right(widget, buffer); + break; + + case KeyEvent.KEYCODE_DPAD_UP: + handled |= up(widget, buffer); + break; + + case KeyEvent.KEYCODE_DPAD_DOWN: + handled |= down(widget, buffer); + break; + } + + return handled; + } + + public boolean onKeyUp(TextView widget, Spannable buffer, int keyCode, KeyEvent event) { + return false; + } + + public boolean onTouchEvent(TextView widget, Spannable buffer, + MotionEvent event) { + return Touch.onTouchEvent(widget, buffer, event); + } + + public boolean onTrackballEvent(TextView widget, Spannable buffer, + MotionEvent event) { + boolean handled = false; + int x = (int) event.getX(); + int y = (int) event.getY(); + + for (; y < 0; y++) { + handled |= up(widget, buffer); + } + for (; y > 0; y--) { + handled |= down(widget, buffer); + } + + for (; x < 0; x++) { + handled |= left(widget, buffer); + } + for (; x > 0; x--) { + handled |= right(widget, buffer); + } + + return handled; + } + + public void initialize(TextView widget, Spannable text) { } + + public boolean canSelectArbitrarily() { + return false; + } + + public void onTakeFocus(TextView widget, Spannable text, int dir) { + Layout layout = widget.getLayout(); + + if (layout != null && (dir & View.FOCUS_FORWARD) != 0) { + widget.scrollTo(widget.getScrollX(), + layout.getLineTop(0)); + } + if (layout != null && (dir & View.FOCUS_BACKWARD) != 0) { + int padding = widget.getTotalPaddingTop() + + widget.getTotalPaddingBottom(); + int line = layout.getLineCount() - 1; + + widget.scrollTo(widget.getScrollX(), + layout.getLineTop(line+1) - + (widget.getHeight() - padding)); + } + } + + public static MovementMethod getInstance() { + if (sInstance == null) + sInstance = new ScrollingMovementMethod(); + + return sInstance; + } + + private static ScrollingMovementMethod sInstance; +} diff --git a/core/java/android/text/method/SingleLineTransformationMethod.java b/core/java/android/text/method/SingleLineTransformationMethod.java new file mode 100644 index 0000000..a4fcf15 --- /dev/null +++ b/core/java/android/text/method/SingleLineTransformationMethod.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2006 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.method; + +import android.graphics.Rect; +import android.text.Editable; +import android.text.GetChars; +import android.text.Spannable; +import android.text.Spanned; +import android.text.SpannedString; +import android.text.TextUtils; +import android.view.View; + +/** + * This transformation method causes any newline characters (\n) to be + * displayed as spaces instead of causing line breaks. + */ +public class SingleLineTransformationMethod +extends ReplacementTransformationMethod { + private static char[] ORIGINAL = new char[] { '\n' }; + private static char[] REPLACEMENT = new char[] { ' ' }; + + /** + * The character to be replaced is \n. + */ + protected char[] getOriginal() { + return ORIGINAL; + } + + /** + * The character \n is replaced with is space. + */ + protected char[] getReplacement() { + return REPLACEMENT; + } + + public static SingleLineTransformationMethod getInstance() { + if (sInstance != null) + return sInstance; + + sInstance = new SingleLineTransformationMethod(); + return sInstance; + } + + private static SingleLineTransformationMethod sInstance; +} diff --git a/core/java/android/text/method/TextKeyListener.java b/core/java/android/text/method/TextKeyListener.java new file mode 100644 index 0000000..012e41d --- /dev/null +++ b/core/java/android/text/method/TextKeyListener.java @@ -0,0 +1,339 @@ +/* + * Copyright (C) 2007 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.method; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.ContentObserver; +import android.os.Handler; +import android.provider.Settings; +import android.provider.Settings.System; +import android.text.*; +import android.view.KeyCharacterMap; +import android.view.KeyEvent; +import android.view.View; + +import java.lang.ref.WeakReference; + +/** + * This is the key listener for typing normal text. It delegates to + * other key listeners appropriate to the current keyboard and language. + */ +public class TextKeyListener extends BaseKeyListener implements SpanWatcher { + private static TextKeyListener[] sInstance = + new TextKeyListener[Capitalize.values().length * 2]; + + /* package */ static final Object ACTIVE = new Object(); + /* package */ static final Object CAPPED = new Object(); + /* package */ static final Object INHIBIT_REPLACEMENT = new Object(); + /* package */ static final Object LAST_TYPED = new Object(); + + private Capitalize mAutoCap; + private boolean mAutoText; + + private int mPrefs; + private boolean mPrefsInited; + + /* package */ static final int AUTO_CAP = 1; + /* package */ static final int AUTO_TEXT = 2; + /* package */ static final int AUTO_PERIOD = 4; + /* package */ static final int SHOW_PASSWORD = 8; + private WeakReference<ContentResolver> mResolver; + private TextKeyListener.SettingsObserver mObserver; + + /** + * Creates a new TextKeyListener with the specified capitalization + * and correction properties. + * + * @param cap when, if ever, to automatically capitalize. + * @param autotext whether to automatically do spelling corrections. + */ + public TextKeyListener(Capitalize cap, boolean autotext) { + mAutoCap = cap; + mAutoText = autotext; + } + + /** + * Returns a new or existing instance with the specified capitalization + * and correction properties. + * + * @param cap when, if ever, to automatically capitalize. + * @param autotext whether to automatically do spelling corrections. + */ + public static TextKeyListener getInstance(boolean autotext, + Capitalize cap) { + int off = cap.ordinal() * 2 + (autotext ? 1 : 0); + + if (sInstance[off] == null) { + sInstance[off] = new TextKeyListener(cap, autotext); + } + + return sInstance[off]; + } + + /** + * Returns a new or existing instance with no automatic capitalization + * or correction. + */ + public static TextKeyListener getInstance() { + return getInstance(false, Capitalize.NONE); + } + + /** + * Returns whether it makes sense to automatically capitalize at the + * specified position in the specified text, with the specified rules. + * + * @param cap the capitalization rules to consider. + * @param cs the text in which an insertion is being made. + * @param off the offset into that text where the insertion is being made. + * + * @return whether the character being inserted should be capitalized. + */ + public static boolean shouldCap(Capitalize cap, CharSequence cs, int off) { + int i; + char c; + + if (cap == Capitalize.NONE) { + return false; + } + if (cap == Capitalize.CHARACTERS) { + return true; + } + + // Back over allowed opening punctuation. + + for (i = off; i > 0; i--) { + c = cs.charAt(i - 1); + + if (c != '"' && c != '(' && c != '[' && c != '\'') { + break; + } + } + + // Start of paragraph, with optional whitespace. + + int j = i; + while (j > 0 && ((c = cs.charAt(j - 1)) == ' ' || c == '\t')) { + j--; + } + if (j == 0 || cs.charAt(j - 1) == '\n') { + return true; + } + + // Or start of word if we are that style. + + if (cap == Capitalize.WORDS) { + return i != j; + } + + // There must be a space if not the start of paragraph. + + if (i == j) { + return false; + } + + // Back over allowed closing punctuation. + + for (; j > 0; j--) { + c = cs.charAt(j - 1); + + if (c != '"' && c != ')' && c != ']' && c != '\'') { + break; + } + } + + if (j > 0) { + c = cs.charAt(j - 1); + + if (c == '.' || c == '?' || c == '!') { + // Do not capitalize if the word ends with a period but + // also contains a period, in which case it is an abbreviation. + + if (c == '.') { + for (int k = j - 2; k >= 0; k--) { + c = cs.charAt(k); + + if (c == '.') { + return false; + } + + if (!Character.isLetter(c)) { + break; + } + } + } + + return true; + } + } + + return false; + } + + @Override + public boolean onKeyDown(View view, Editable content, + int keyCode, KeyEvent event) { + KeyListener im = getKeyListener(event); + + return im.onKeyDown(view, content, keyCode, event); + } + + @Override + public boolean onKeyUp(View view, Editable content, + int keyCode, KeyEvent event) { + KeyListener im = getKeyListener(event); + + return im.onKeyUp(view, content, keyCode, event); + } + + /** + * Clear all the input state (autotext, autocap, multitap, undo) + * from the specified Editable, going beyond Editable.clear(), which + * just clears the text but not the input state. + * + * @param e the buffer whose text and state are to be cleared. + */ + public static void clear(Editable e) { + e.clear(); + e.removeSpan(ACTIVE); + e.removeSpan(CAPPED); + e.removeSpan(INHIBIT_REPLACEMENT); + e.removeSpan(LAST_TYPED); + + QwertyKeyListener.Replaced[] repl = e.getSpans(0, e.length(), + QwertyKeyListener.Replaced.class); + final int count = repl.length; + for (int i = 0; i < count; i++) { + e.removeSpan(repl[i]); + } + } + + public void onSpanAdded(Spannable s, Object what, int start, int end) { } + public void onSpanRemoved(Spannable s, Object what, int start, int end) { } + + public void onSpanChanged(Spannable s, Object what, int start, int end, + int st, int en) { + if (what == Selection.SELECTION_END) { + s.removeSpan(ACTIVE); + } + } + + private KeyListener getKeyListener(KeyEvent event) { + KeyCharacterMap kmap = KeyCharacterMap.load(event.getKeyboardDevice()); + int kind = kmap.getKeyboardType(); + + if (kind == KeyCharacterMap.ALPHA) { + return QwertyKeyListener.getInstance(mAutoText, mAutoCap); + } else if (kind == KeyCharacterMap.NUMERIC) { + return MultiTapKeyListener.getInstance(mAutoText, mAutoCap); + } + + return NullKeyListener.getInstance(); + } + + public enum Capitalize { + NONE, SENTENCES, WORDS, CHARACTERS, + } + + private static class NullKeyListener implements KeyListener + { + public boolean onKeyDown(View view, Editable content, + int keyCode, KeyEvent event) { + return false; + } + + public boolean onKeyUp(View view, Editable content, int keyCode, + KeyEvent event) { + return false; + } + + public static NullKeyListener getInstance() { + if (sInstance != null) + return sInstance; + + sInstance = new NullKeyListener(); + return sInstance; + } + + private static NullKeyListener sInstance; + } + + public void release() { + if (mResolver != null) { + final ContentResolver contentResolver = mResolver.get(); + if (contentResolver != null) { + contentResolver.unregisterContentObserver(mObserver); + mResolver.clear(); + } + mObserver = null; + mResolver = null; + mPrefsInited = false; + } + } + + private void initPrefs(Context context) { + final ContentResolver contentResolver = context.getContentResolver(); + mResolver = new WeakReference<ContentResolver>(contentResolver); + mObserver = new SettingsObserver(); + contentResolver.registerContentObserver(Settings.System.CONTENT_URI, true, mObserver); + + updatePrefs(contentResolver); + mPrefsInited = true; + } + + private class SettingsObserver extends ContentObserver { + public SettingsObserver() { + super(new Handler()); + } + + @Override + public void onChange(boolean selfChange) { + if (mResolver != null) { + final ContentResolver contentResolver = mResolver.get(); + if (contentResolver == null) { + mPrefsInited = false; + } else { + updatePrefs(contentResolver); + } + } else { + mPrefsInited = false; + } + } + } + + private void updatePrefs(ContentResolver resolver) { + boolean cap = System.getInt(resolver, System.TEXT_AUTO_CAPS, 1) > 0; + boolean text = System.getInt(resolver, System.TEXT_AUTO_REPLACE, 1) > 0; + boolean period = System.getInt(resolver, System.TEXT_AUTO_PUNCTUATE, 1) > 0; + boolean pw = System.getInt(resolver, System.TEXT_SHOW_PASSWORD, 1) > 0; + + mPrefs = (cap ? AUTO_CAP : 0) | + (text ? AUTO_TEXT : 0) | + (period ? AUTO_PERIOD : 0) | + (pw ? SHOW_PASSWORD : 0); + } + + /* package */ int getPrefs(Context context) { + synchronized (this) { + if (!mPrefsInited || mResolver.get() == null) { + initPrefs(context); + } + } + + return mPrefs; + } +} diff --git a/core/java/android/text/method/TimeKeyListener.java b/core/java/android/text/method/TimeKeyListener.java new file mode 100644 index 0000000..9ba1fe6 --- /dev/null +++ b/core/java/android/text/method/TimeKeyListener.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2006 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.method; + +import android.view.KeyEvent; + +/** + * For entering times in a text field. + */ +public class TimeKeyListener extends NumberKeyListener +{ + @Override + protected char[] getAcceptedChars() + { + return CHARACTERS; + } + + public static TimeKeyListener getInstance() { + if (sInstance != null) + return sInstance; + + sInstance = new TimeKeyListener(); + return sInstance; + } + + /** + * The characters that are used. + * + * @see KeyEvent#getMatch + * @see #getAcceptedChars + */ + public static final char[] CHARACTERS = new char[] { + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'm', + 'p', ':' + }; + + private static TimeKeyListener sInstance; +} diff --git a/core/java/android/text/method/Touch.java b/core/java/android/text/method/Touch.java new file mode 100644 index 0000000..bd01728 --- /dev/null +++ b/core/java/android/text/method/Touch.java @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2008 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.method; + +import android.text.Layout; +import android.text.Spannable; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.widget.TextView; + +public class Touch { + private Touch() { } + + /** + * Scrolls the specified widget to the specified coordinates, except + * constrains the X scrolling position to the horizontal regions of + * the text that will be visible after scrolling to the specified + * Y position. + */ + public static void scrollTo(TextView widget, Layout layout, int x, int y) { + int padding = widget.getTotalPaddingTop() + + widget.getTotalPaddingBottom(); + int top = layout.getLineForVertical(y); + int bottom = layout.getLineForVertical(y + widget.getHeight() - + padding); + + int left = Integer.MAX_VALUE; + int right = 0; + + for (int i = top; i <= bottom; i++) { + left = (int) Math.min(left, layout.getLineLeft(i)); + right = (int) Math.max(right, layout.getLineRight(i)); + } + + padding = widget.getTotalPaddingLeft() + widget.getTotalPaddingRight(); + x = Math.min(x, right - (widget.getWidth() - padding)); + x = Math.max(x, left); + + widget.scrollTo(x, y); + } + + /** + * Handles touch events for dragging. You may want to do other actions + * like moving the cursor on touch as well. + */ + public static boolean onTouchEvent(TextView widget, Spannable buffer, + MotionEvent event) { + DragState[] ds; + + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + buffer.setSpan(new DragState(event.getX(), event.getY()), + 0, 0, Spannable.SPAN_MARK_MARK); + return true; + + case MotionEvent.ACTION_UP: + ds = buffer.getSpans(0, buffer.length(), DragState.class); + + for (int i = 0; i < ds.length; i++) { + buffer.removeSpan(ds[i]); + } + + if (ds.length > 0 && ds[0].mUsed) { + return true; + } else { + return false; + } + + case MotionEvent.ACTION_MOVE: + ds = buffer.getSpans(0, buffer.length(), DragState.class); + + if (ds.length > 0) { + if (ds[0].mFarEnough == false) { + int slop = ViewConfiguration.getTouchSlop(); + + if (Math.abs(event.getX() - ds[0].mX) >= slop || + Math.abs(event.getY() - ds[0].mY) >= slop) { + ds[0].mFarEnough = true; + } + } + + if (ds[0].mFarEnough) { + ds[0].mUsed = true; + + float dx = ds[0].mX - event.getX(); + float dy = ds[0].mY - event.getY(); + + ds[0].mX = event.getX(); + ds[0].mY = event.getY(); + + int nx = widget.getScrollX() + (int) dx; + int ny = widget.getScrollY() + (int) dy; + + int padding = widget.getTotalPaddingTop() + + widget.getTotalPaddingBottom(); + Layout layout = widget.getLayout(); + + ny = Math.min(ny, layout.getHeight() - (widget.getHeight() - + padding)); + ny = Math.max(ny, 0); + + scrollTo(widget, layout, nx, ny); + widget.cancelLongPress(); + return true; + } + } + } + + return false; + } + + private static class DragState { + public float mX; + public float mY; + public boolean mFarEnough; + public boolean mUsed; + + public DragState(float x, float y) { + mX = x; + mY = y; + } + } +} diff --git a/core/java/android/text/method/TransformationMethod.java b/core/java/android/text/method/TransformationMethod.java new file mode 100644 index 0000000..9f51c2a --- /dev/null +++ b/core/java/android/text/method/TransformationMethod.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2006 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.method; + +import android.graphics.Rect; +import android.view.View; +import android.widget.TextView; + +/** + * TextView uses TransformationMethods to do things like replacing the + * characters of passwords with dots, or keeping the newline characters + * from causing line breaks in single-line text fields. + */ +public interface TransformationMethod +{ + /** + * Returns a CharSequence that is a transformation of the source text -- + * for example, replacing each character with a dot in a password field. + * Beware that the returned text must be exactly the same length as + * the source text, and that if the source text is Editable, the returned + * text must mirror it dynamically instead of doing a one-time copy. + */ + public CharSequence getTransformation(CharSequence source, View view); + + /** + * This method is called when the TextView that uses this + * TransformationMethod gains or loses focus. + */ + public void onFocusChanged(View view, CharSequence sourceText, + boolean focused, int direction, + Rect previouslyFocusedRect); +} diff --git a/core/java/android/text/method/package.html b/core/java/android/text/method/package.html new file mode 100644 index 0000000..93698b8 --- /dev/null +++ b/core/java/android/text/method/package.html @@ -0,0 +1,21 @@ +<html> +<body> + +<p>Provides classes that monitor or modify keypad input.</p> +<p>You can use these classes to modify the type of keypad entry +for your application, or decipher the keypresses entered for your specific +entry method. For example:</p> +<pre> +// Set the text to password display style: +EditText txtView = (EditText)findViewById(R.id.text); +txtView.setTransformationMethod(PasswordTransformationMethod.getInstance()); + +//Set the input style to numbers, rather than qwerty keyboard style. +txtView.setInputMethod(DigitsInputMethod.getInstance()); + +// Find out whether the caps lock is on. +// 0 is no, 1 is yes, 2 is caps lock on. +int active = MultiTapInputMethod.getCapsActive(txtView.getText()); +</pre> +</body> +</html> diff --git a/core/java/android/text/package.html b/core/java/android/text/package.html new file mode 100644 index 0000000..162dcd8 --- /dev/null +++ b/core/java/android/text/package.html @@ -0,0 +1,13 @@ +<html> +<body> + +<p>Provides classes used to render or track text and text spans on the screen.</p> +<p>You can use these classes to design your own widgets that manage text, +to handle arbitrary text spans for changes, or to handle drawing yourself +for an existing widget.</p> +<p>The Span… interfaces and classes are used to create or manage spans of +text in a View item. You can use these to style the text or background, or to +listen for changes. If creating your own widget, extend DynamicLayout, to manages +the actual wrapping and drawing of your text. +</body> +</html> diff --git a/core/java/android/text/style/AbsoluteSizeSpan.java b/core/java/android/text/style/AbsoluteSizeSpan.java new file mode 100644 index 0000000..8f6ed5a --- /dev/null +++ b/core/java/android/text/style/AbsoluteSizeSpan.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2008 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.graphics.Paint; +import android.text.TextPaint; + +public class AbsoluteSizeSpan extends MetricAffectingSpan { + + private int mSize; + + public AbsoluteSizeSpan(int size) { + mSize = size; + } + + public int getSize() { + return mSize; + } + + @Override + public void updateDrawState(TextPaint ds) { + ds.setTextSize(mSize); + } + + @Override + public void updateMeasureState(TextPaint ds) { + ds.setTextSize(mSize); + } +} diff --git a/core/java/android/text/style/AlignmentSpan.java b/core/java/android/text/style/AlignmentSpan.java new file mode 100644 index 0000000..d51edcc --- /dev/null +++ b/core/java/android/text/style/AlignmentSpan.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2006 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.text.Layout; + +public interface AlignmentSpan +extends ParagraphStyle +{ + public Layout.Alignment getAlignment(); + + public static class Standard + implements AlignmentSpan + { + public Standard(Layout.Alignment align) { + mAlignment = align; + } + + public Layout.Alignment getAlignment() { + return mAlignment; + } + + private Layout.Alignment mAlignment; + } +} diff --git a/core/java/android/text/style/BackgroundColorSpan.java b/core/java/android/text/style/BackgroundColorSpan.java new file mode 100644 index 0000000..be6ef77 --- /dev/null +++ b/core/java/android/text/style/BackgroundColorSpan.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2006 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.text.TextPaint; + +public class BackgroundColorSpan extends CharacterStyle { + + private int mColor; + + public BackgroundColorSpan(int color) { + mColor = color; + } + + public int getBackgroundColor() { + return mColor; + } + + @Override + public void updateDrawState(TextPaint ds) { + ds.bgColor = mColor; + } +} diff --git a/core/java/android/text/style/BulletSpan.java b/core/java/android/text/style/BulletSpan.java new file mode 100644 index 0000000..70c4d33 --- /dev/null +++ b/core/java/android/text/style/BulletSpan.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2006 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.graphics.Canvas; +import android.graphics.Paint; +import android.text.Layout; +import android.text.Spanned; + +public class BulletSpan implements LeadingMarginSpan { + + public BulletSpan() { + mGapWidth = STANDARD_GAP_WIDTH; + } + + public BulletSpan(int gapWidth) { + mGapWidth = gapWidth; + } + + public BulletSpan(int gapWidth, int color) { + mGapWidth = gapWidth; + mWantColor = true; + mColor = color; + } + + public int getLeadingMargin(boolean first) { + return 2 * BULLET_RADIUS + mGapWidth; + } + + public void drawLeadingMargin(Canvas c, Paint p, int x, int dir, + int top, int baseline, int bottom, + CharSequence text, int start, int end, + boolean first, Layout l) { + if (((Spanned) text).getSpanStart(this) == start) { + Paint.Style style = p.getStyle(); + int oldcolor = 0; + + if (mWantColor) { + oldcolor = p.getColor(); + p.setColor(mColor); + } + + p.setStyle(Paint.Style.FILL); + + c.drawCircle(x + dir * BULLET_RADIUS, (top + bottom) / 2.0f, + BULLET_RADIUS, p); + + if (mWantColor) { + p.setColor(oldcolor); + } + + p.setStyle(style); + } + } + + private int mGapWidth; + private boolean mWantColor; + private int mColor; + + private static final int BULLET_RADIUS = 3; + public static final int STANDARD_GAP_WIDTH = 2; +} diff --git a/core/java/android/text/style/CharacterStyle.java b/core/java/android/text/style/CharacterStyle.java new file mode 100644 index 0000000..7620d29 --- /dev/null +++ b/core/java/android/text/style/CharacterStyle.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2006 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.graphics.Paint; +import android.text.TextPaint; + +/** + * The classes that affect character-level text formatting extend this + * class. Most also extend {@link MetricAffectingSpan}. + */ +public abstract class CharacterStyle { + public abstract void updateDrawState(TextPaint tp); + + /** + * A given CharacterStyle can only applied to a single region of a given + * Spanned. If you need to attach the same CharacterStyle to multiple + * regions, you can use this method to wrap it with a new object that + * will have the same effect but be a distinct object so that it can + * also be attached without conflict. + */ + public static CharacterStyle wrap(CharacterStyle cs) { + if (cs instanceof MetricAffectingSpan) { + return new MetricAffectingSpan.Passthrough((MetricAffectingSpan) cs); + } else { + return new Passthrough(cs); + } + } + + /** + * Returns "this" for most CharacterStyles, but for CharacterStyles + * that were generated by {@link #wrap}, returns the underlying + * CharacterStyle. + */ + public CharacterStyle getUnderlying() { + return this; + } + + /** + * A Passthrough CharacterStyle is one that + * passes {@link #updateDrawState} calls through to the + * specified CharacterStyle while still being a distinct object, + * and is therefore able to be attached to the same Spannable + * to which the specified CharacterStyle is already attached. + */ + private static class Passthrough extends CharacterStyle { + private CharacterStyle mStyle; + + /** + * Creates a new Passthrough of the specfied CharacterStyle. + */ + public Passthrough(CharacterStyle cs) { + mStyle = cs; + } + + /** + * Passes updateDrawState through to the underlying CharacterStyle. + */ + @Override + public void updateDrawState(TextPaint tp) { + mStyle.updateDrawState(tp); + } + + /** + * Returns the CharacterStyle underlying this one, or the one + * underlying it if it too is a Passthrough. + */ + @Override + public CharacterStyle getUnderlying() { + return mStyle.getUnderlying(); + } + } +} diff --git a/core/java/android/text/style/ClickableSpan.java b/core/java/android/text/style/ClickableSpan.java new file mode 100644 index 0000000..a232ed7 --- /dev/null +++ b/core/java/android/text/style/ClickableSpan.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2008 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.text.TextPaint; +import android.view.View; + +/** + * If an object of this type is attached to the text of a TextView + * with a movement method of LinkMovementMethod, the affected spans of + * text can be selected. If clicked, the {@link #onClick} method will + * be called. + */ +public abstract class ClickableSpan extends CharacterStyle { + + /** + * Performs the click action associated with this span. + */ + public abstract void onClick(View widget); + + /** + * Makes the text underlined and in the link color. + */ + @Override + public void updateDrawState(TextPaint ds) { + ds.setColor(ds.linkColor); + ds.setUnderlineText(true); + } +} diff --git a/core/java/android/text/style/DrawableMarginSpan.java b/core/java/android/text/style/DrawableMarginSpan.java new file mode 100644 index 0000000..3c471a5 --- /dev/null +++ b/core/java/android/text/style/DrawableMarginSpan.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2006 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.graphics.drawable.Drawable; +import android.graphics.Paint; +import android.graphics.Canvas; +import android.graphics.RectF; +import android.text.Spanned; +import android.text.Layout; + +public class DrawableMarginSpan +implements LeadingMarginSpan, LineHeightSpan +{ + public DrawableMarginSpan(Drawable b) { + mDrawable = b; + } + + public DrawableMarginSpan(Drawable b, int pad) { + mDrawable = b; + mPad = pad; + } + + public int getLeadingMargin(boolean first) { + return mDrawable.getIntrinsicWidth() + mPad; + } + + public void drawLeadingMargin(Canvas c, Paint p, int x, int dir, + int top, int baseline, int bottom, + CharSequence text, int start, int end, + boolean first, Layout layout) { + int st = ((Spanned) text).getSpanStart(this); + int ix = (int)x; + int itop = (int)layout.getLineTop(layout.getLineForOffset(st)); + + int dw = mDrawable.getIntrinsicWidth(); + int dh = mDrawable.getIntrinsicHeight(); + + if (dir < 0) + x -= dw; + + // XXX What to do about Paint? + mDrawable.setBounds(ix, itop, ix+dw, itop+dh); + mDrawable.draw(c); + } + + public void chooseHeight(CharSequence text, int start, int end, + int istartv, int v, + Paint.FontMetricsInt fm) { + if (end == ((Spanned) text).getSpanEnd(this)) { + int ht = mDrawable.getIntrinsicHeight(); + + int need = ht - (v + fm.descent - fm.ascent - istartv); + if (need > 0) + fm.descent += need; + + need = ht - (v + fm.bottom - fm.top - istartv); + if (need > 0) + fm.bottom += need; + } + } + + private Drawable mDrawable; + private int mPad; +} diff --git a/core/java/android/text/style/DynamicDrawableSpan.java b/core/java/android/text/style/DynamicDrawableSpan.java new file mode 100644 index 0000000..3bcc335 --- /dev/null +++ b/core/java/android/text/style/DynamicDrawableSpan.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2006 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 java.lang.ref.WeakReference; + +import android.graphics.drawable.Drawable; +import android.graphics.Canvas; +import android.graphics.Paint; + +/** + * + */ +public abstract class DynamicDrawableSpan +extends ReplacementSpan +{ + /** + * Your subclass must implement this method to provide the bitmap + * to be drawn. The dimensions of the bitmap must be the same + * from each call to the next. + */ + public abstract Drawable getDrawable(); + + public int getSize(Paint paint, CharSequence text, + int start, int end, + Paint.FontMetricsInt fm) { + Drawable b = getCachedDrawable(); + + if (fm != null) { + fm.ascent = -b.getIntrinsicHeight(); + fm.descent = 0; + + fm.top = fm.ascent; + fm.bottom = 0; + } + + return b.getIntrinsicWidth(); + } + + public void draw(Canvas canvas, CharSequence text, + int start, int end, float x, + int top, int y, int bottom, Paint paint) { + Drawable b = getCachedDrawable(); + canvas.save(); + + canvas.translate(x, bottom-b.getIntrinsicHeight());; + b.draw(canvas); + canvas.restore(); + } + + private Drawable getCachedDrawable() { + WeakReference wr = mDrawableRef; + Drawable b = null; + + if (wr != null) + b = (Drawable) wr.get(); + + if (b == null) { + b = getDrawable(); + mDrawableRef = new WeakReference(b); + } + + return b; + } + + private WeakReference mDrawableRef; +} + diff --git a/core/java/android/text/style/ForegroundColorSpan.java b/core/java/android/text/style/ForegroundColorSpan.java new file mode 100644 index 0000000..5cccd9c --- /dev/null +++ b/core/java/android/text/style/ForegroundColorSpan.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2006 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.graphics.Paint; +import android.text.TextPaint; + +public class ForegroundColorSpan extends CharacterStyle { + + private int mColor; + + public ForegroundColorSpan(int color) { + mColor = color; + } + + public int getForegroundColor() { + return mColor; + } + + @Override + public void updateDrawState(TextPaint ds) { + ds.setColor(mColor); + } +} diff --git a/core/java/android/text/style/IconMarginSpan.java b/core/java/android/text/style/IconMarginSpan.java new file mode 100644 index 0000000..c786a17 --- /dev/null +++ b/core/java/android/text/style/IconMarginSpan.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2006 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.graphics.Paint; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.RectF; +import android.text.Spanned; +import android.text.Layout; + +public class IconMarginSpan +implements LeadingMarginSpan, LineHeightSpan +{ + public IconMarginSpan(Bitmap b) { + mBitmap = b; + } + + public IconMarginSpan(Bitmap b, int pad) { + mBitmap = b; + mPad = pad; + } + + public int getLeadingMargin(boolean first) { + return mBitmap.getWidth() + mPad; + } + + public void drawLeadingMargin(Canvas c, Paint p, int x, int dir, + int top, int baseline, int bottom, + CharSequence text, int start, int end, + boolean first, Layout layout) { + int st = ((Spanned) text).getSpanStart(this); + int itop = layout.getLineTop(layout.getLineForOffset(st)); + + if (dir < 0) + x -= mBitmap.getWidth(); + + c.drawBitmap(mBitmap, x, itop, p); + } + + public void chooseHeight(CharSequence text, int start, int end, + int istartv, int v, + Paint.FontMetricsInt fm) { + if (end == ((Spanned) text).getSpanEnd(this)) { + int ht = mBitmap.getHeight(); + + int need = ht - (v + fm.descent - fm.ascent - istartv); + if (need > 0) + fm.descent += need; + + need = ht - (v + fm.bottom - fm.top - istartv); + if (need > 0) + fm.bottom += need; + } + } + + private Bitmap mBitmap; + private int mPad; +} diff --git a/core/java/android/text/style/ImageSpan.java b/core/java/android/text/style/ImageSpan.java new file mode 100644 index 0000000..de067b1 --- /dev/null +++ b/core/java/android/text/style/ImageSpan.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2006 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.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.util.Log; + +import java.io.InputStream; + +public class ImageSpan extends DynamicDrawableSpan { + private Drawable mDrawable; + private Uri mContentUri; + private int mResourceId; + private Context mContext; + private String mSource; + + + public ImageSpan(Bitmap b) { + mDrawable = new BitmapDrawable(b); + mDrawable.setBounds(0, 0, mDrawable.getIntrinsicWidth(), + mDrawable.getIntrinsicHeight()); + } + + public ImageSpan(Drawable d) { + mDrawable = d; + } + + public ImageSpan(Drawable d, String source) { + mDrawable = d; + mSource = source; + } + + public ImageSpan(Context context, Uri uri) { + mContext = context; + mContentUri = uri; + } + + public ImageSpan(Context context, int resourceId) { + mContext = context; + mResourceId = resourceId; + } + + @Override + public Drawable getDrawable() { + Drawable drawable = null; + + if (mDrawable != null) { + drawable = mDrawable; + } else if (mContentUri != null) { + Bitmap bitmap = null; + try { + InputStream is = mContext.getContentResolver().openInputStream( + mContentUri); + bitmap = BitmapFactory.decodeStream(is); + drawable = new BitmapDrawable(bitmap); + is.close(); + } catch (Exception e) { + Log.e("sms", "Failed to loaded content " + mContentUri, e); + } + } else { + try { + drawable = mContext.getResources().getDrawable(mResourceId); + drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), + drawable.getIntrinsicHeight()); + } catch (Exception e) { + Log.e("sms", "Unable to find resource: " + mResourceId); + } + } + + return drawable; + } + + /** + * Returns the source string that was saved during construction. + */ + public String getSource() { + return mSource; + } + +} diff --git a/core/java/android/text/style/LeadingMarginSpan.java b/core/java/android/text/style/LeadingMarginSpan.java new file mode 100644 index 0000000..85a27dc --- /dev/null +++ b/core/java/android/text/style/LeadingMarginSpan.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2006 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.graphics.Paint; +import android.graphics.Canvas; +import android.text.Layout; + +public interface LeadingMarginSpan +extends ParagraphStyle +{ + public int getLeadingMargin(boolean first); + public void drawLeadingMargin(Canvas c, Paint p, + int x, int dir, + int top, int baseline, int bottom, + CharSequence text, int start, int end, + boolean first, Layout layout); + + public static class Standard + implements LeadingMarginSpan + { + public Standard(int first, int rest) { + mFirst = first; + mRest = rest; + } + + public Standard(int every) { + this(every, every); + } + + public int getLeadingMargin(boolean first) { + return first ? mFirst : mRest; + } + + public void drawLeadingMargin(Canvas c, Paint p, + int x, int dir, + int top, int baseline, int bottom, + CharSequence text, int start, int end, + boolean first, Layout layout) { + ; + } + + private int mFirst, mRest; + } +} diff --git a/core/java/android/text/style/LineBackgroundSpan.java b/core/java/android/text/style/LineBackgroundSpan.java new file mode 100644 index 0000000..854aeaf --- /dev/null +++ b/core/java/android/text/style/LineBackgroundSpan.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2006 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.graphics.Paint; +import android.graphics.Canvas; + +public interface LineBackgroundSpan +extends ParagraphStyle +{ + public void drawBackground(Canvas c, Paint p, + int left, int right, + int top, int baseline, int bottom, + CharSequence text, int start, int end, + int lnum); +} diff --git a/core/java/android/text/style/LineHeightSpan.java b/core/java/android/text/style/LineHeightSpan.java new file mode 100644 index 0000000..c0ef97c --- /dev/null +++ b/core/java/android/text/style/LineHeightSpan.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2006 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.graphics.Paint; +import android.graphics.Canvas; +import android.text.Layout; + +public interface LineHeightSpan +extends ParagraphStyle, WrapTogetherSpan +{ + public void chooseHeight(CharSequence text, int start, int end, + int spanstartv, int v, + Paint.FontMetricsInt fm); +} diff --git a/core/java/android/text/style/MaskFilterSpan.java b/core/java/android/text/style/MaskFilterSpan.java new file mode 100644 index 0000000..781bcec --- /dev/null +++ b/core/java/android/text/style/MaskFilterSpan.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2006 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.graphics.Paint; +import android.graphics.MaskFilter; +import android.text.TextPaint; + +public class MaskFilterSpan extends CharacterStyle { + + private MaskFilter mFilter; + + public MaskFilterSpan(MaskFilter filter) { + mFilter = filter; + } + + public MaskFilter getMaskFilter() { + return mFilter; + } + + @Override + public void updateDrawState(TextPaint ds) { + ds.setMaskFilter(mFilter); + } +} diff --git a/core/java/android/text/style/MetricAffectingSpan.java b/core/java/android/text/style/MetricAffectingSpan.java new file mode 100644 index 0000000..92558eb --- /dev/null +++ b/core/java/android/text/style/MetricAffectingSpan.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2006 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.graphics.Paint; +import android.text.TextPaint; + +/** + * The classes that affect character-level text formatting in a way that + * changes the width or height of characters extend this class. + */ +public abstract class MetricAffectingSpan +extends CharacterStyle +implements UpdateLayout { + + public abstract void updateMeasureState(TextPaint p); + + /** + * Returns "this" for most MetricAffectingSpans, but for + * MetricAffectingSpans that were generated by {@link #wrap}, + * returns the underlying MetricAffectingSpan. + */ + @Override + public MetricAffectingSpan getUnderlying() { + return this; + } + + /** + * A Passthrough MetricAffectingSpan is one that + * passes {@link #updateDrawState} and {@link #updateMeasureState} + * calls through to the specified MetricAffectingSpan + * while still being a distinct object, + * and is therefore able to be attached to the same Spannable + * to which the specified MetricAffectingSpan is already attached. + */ + /* package */ static class Passthrough extends MetricAffectingSpan { + private MetricAffectingSpan mStyle; + + /** + * Creates a new Passthrough of the specfied MetricAffectingSpan. + */ + public Passthrough(MetricAffectingSpan cs) { + mStyle = cs; + } + + /** + * Passes updateDrawState through to the underlying MetricAffectingSpan. + */ + @Override + public void updateDrawState(TextPaint tp) { + mStyle.updateDrawState(tp); + } + + /** + * Passes updateMeasureState through to the underlying MetricAffectingSpan. + */ + @Override + public void updateMeasureState(TextPaint tp) { + mStyle.updateMeasureState(tp); + } + + /** + * Returns the MetricAffectingSpan underlying this one, or the one + * underlying it if it too is a Passthrough. + */ + @Override + public MetricAffectingSpan getUnderlying() { + return mStyle.getUnderlying(); + } + } +} diff --git a/core/java/android/text/style/ParagraphStyle.java b/core/java/android/text/style/ParagraphStyle.java new file mode 100644 index 0000000..423156e --- /dev/null +++ b/core/java/android/text/style/ParagraphStyle.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2006 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; + +/** + * The classes that affect paragraph-level text formatting implement + * this interface. + */ +public interface ParagraphStyle +{ + +} diff --git a/core/java/android/text/style/QuoteSpan.java b/core/java/android/text/style/QuoteSpan.java new file mode 100644 index 0000000..3f4a32f --- /dev/null +++ b/core/java/android/text/style/QuoteSpan.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2006 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.graphics.Paint; +import android.graphics.Canvas; +import android.graphics.RectF; +import android.text.Layout; + +public class QuoteSpan +implements LeadingMarginSpan +{ + private static final int STRIPE_WIDTH = 2; + private static final int GAP_WIDTH = 2; + + private int mColor = 0xff0000ff; + + public QuoteSpan() { + super(); + } + + public QuoteSpan(int color) { + this(); + mColor = color; + } + + public int getColor() { + return mColor; + } + + public int getLeadingMargin(boolean first) { + return STRIPE_WIDTH + GAP_WIDTH; + } + + public void drawLeadingMargin(Canvas c, Paint p, int x, int dir, + int top, int baseline, int bottom, + CharSequence text, int start, int end, + boolean first, Layout layout) { + Paint.Style style = p.getStyle(); + int color = p.getColor(); + + p.setStyle(Paint.Style.FILL); + p.setColor(mColor); + + c.drawRect(x, top, x + dir * STRIPE_WIDTH, bottom, p); + + p.setStyle(style); + p.setColor(color); + } +} diff --git a/core/java/android/text/style/RasterizerSpan.java b/core/java/android/text/style/RasterizerSpan.java new file mode 100644 index 0000000..193c700 --- /dev/null +++ b/core/java/android/text/style/RasterizerSpan.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2007 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.graphics.Paint; +import android.graphics.Rasterizer; +import android.text.TextPaint; + +public class RasterizerSpan extends CharacterStyle { + + private Rasterizer mRasterizer; + + public RasterizerSpan(Rasterizer r) { + mRasterizer = r; + } + + public Rasterizer getRasterizer() { + return mRasterizer; + } + + @Override + public void updateDrawState(TextPaint ds) { + ds.setRasterizer(mRasterizer); + } +} diff --git a/core/java/android/text/style/RelativeSizeSpan.java b/core/java/android/text/style/RelativeSizeSpan.java new file mode 100644 index 0000000..a8ad076 --- /dev/null +++ b/core/java/android/text/style/RelativeSizeSpan.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2006 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.graphics.Paint; +import android.text.TextPaint; + +public class RelativeSizeSpan extends MetricAffectingSpan { + + private float mProportion; + + public RelativeSizeSpan(float proportion) { + mProportion = proportion; + } + + public float getSizeChange() { + return mProportion; + } + + @Override + public void updateDrawState(TextPaint ds) { + ds.setTextSize(ds.getTextSize() * mProportion); + } + + @Override + public void updateMeasureState(TextPaint ds) { + ds.setTextSize(ds.getTextSize() * mProportion); + } +} diff --git a/core/java/android/text/style/ReplacementSpan.java b/core/java/android/text/style/ReplacementSpan.java new file mode 100644 index 0000000..26c725f --- /dev/null +++ b/core/java/android/text/style/ReplacementSpan.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2006 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.graphics.Paint; +import android.graphics.Canvas; +import android.text.TextPaint; + +public abstract class ReplacementSpan extends MetricAffectingSpan { + + public abstract int getSize(Paint paint, CharSequence text, + int start, int end, + Paint.FontMetricsInt fm); + public abstract void draw(Canvas canvas, CharSequence text, + int start, int end, float x, + int top, int y, int bottom, Paint paint); + + /** + * This method does nothing, since ReplacementSpans are measured + * explicitly instead of affecting Paint properties. + */ + public void updateMeasureState(TextPaint p) { } + + /** + * This method does nothing, since ReplacementSpans are drawn + * explicitly instead of affecting Paint properties. + */ + public void updateDrawState(TextPaint ds) { } +} diff --git a/core/java/android/text/style/ScaleXSpan.java b/core/java/android/text/style/ScaleXSpan.java new file mode 100644 index 0000000..ac9e35d --- /dev/null +++ b/core/java/android/text/style/ScaleXSpan.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2006 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.graphics.Paint; +import android.text.TextPaint; + +public class ScaleXSpan extends MetricAffectingSpan { + + private float mProportion; + + public ScaleXSpan(float proportion) { + mProportion = proportion; + } + + public float getScaleX() { + return mProportion; + } + + @Override + public void updateDrawState(TextPaint ds) { + ds.setTextScaleX(ds.getTextScaleX() * mProportion); + } + + @Override + public void updateMeasureState(TextPaint ds) { + ds.setTextScaleX(ds.getTextScaleX() * mProportion); + } +} diff --git a/core/java/android/text/style/StrikethroughSpan.java b/core/java/android/text/style/StrikethroughSpan.java new file mode 100644 index 0000000..01ae38c --- /dev/null +++ b/core/java/android/text/style/StrikethroughSpan.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2006 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.text.TextPaint; + +public class StrikethroughSpan extends CharacterStyle { + + @Override + public void updateDrawState(TextPaint ds) { + ds.setStrikeThruText(true); + } +} diff --git a/core/java/android/text/style/StyleSpan.java b/core/java/android/text/style/StyleSpan.java new file mode 100644 index 0000000..cc8b06c --- /dev/null +++ b/core/java/android/text/style/StyleSpan.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2006 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.graphics.Paint; +import android.graphics.Typeface; +import android.text.TextPaint; + +/** + * + * Describes a style in a span. + * Note that styles are cumulative -- if both bold and italic are set in + * separate spans, or if the base style is bold and a span calls for italic, + * you get bold italic. You can't turn off a style from the base style. + * + */ +public class StyleSpan extends MetricAffectingSpan { + + private int mStyle; + + /** + * + * @param style An integer constant describing the style for this span. Examples + * include bold, italic, and normal. Values are constants defined + * in {@link android.graphics.Typeface}. + */ + public StyleSpan(int style) { + mStyle = style; + } + + /** + * Returns the style constant defined in {@link android.graphics.Typeface}. + */ + public int getStyle() { + return mStyle; + } + + @Override + public void updateDrawState(TextPaint ds) { + apply(ds, mStyle); + } + + @Override + public void updateMeasureState(TextPaint paint) { + apply(paint, mStyle); + } + + private static void apply(Paint paint, int style) { + int oldStyle; + + Typeface old = paint.getTypeface(); + if (old == null) { + oldStyle = 0; + } else { + oldStyle = old.getStyle(); + } + + int want = oldStyle | style; + + Typeface tf; + if (old == null) { + tf = Typeface.defaultFromStyle(want); + } else { + tf = Typeface.create(old, want); + } + + int fake = want & ~tf.getStyle(); + + if ((fake & Typeface.BOLD) != 0) { + paint.setFakeBoldText(true); + } + + if ((fake & Typeface.ITALIC) != 0) { + paint.setTextSkewX(-0.25f); + } + + paint.setTypeface(tf); + } +} diff --git a/core/java/android/text/style/SubscriptSpan.java b/core/java/android/text/style/SubscriptSpan.java new file mode 100644 index 0000000..78d6ba9 --- /dev/null +++ b/core/java/android/text/style/SubscriptSpan.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2006 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.text.TextPaint; + +public class SubscriptSpan extends MetricAffectingSpan { + @Override + public void updateDrawState(TextPaint tp) { + tp.baselineShift -= (int) (tp.ascent() / 2); + } + + @Override + public void updateMeasureState(TextPaint tp) { + tp.baselineShift -= (int) (tp.ascent() / 2); + } +} diff --git a/core/java/android/text/style/SuperscriptSpan.java b/core/java/android/text/style/SuperscriptSpan.java new file mode 100644 index 0000000..79be4de --- /dev/null +++ b/core/java/android/text/style/SuperscriptSpan.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2006 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.text.TextPaint; + +public class SuperscriptSpan extends MetricAffectingSpan { + @Override + public void updateDrawState(TextPaint tp) { + tp.baselineShift += (int) (tp.ascent() / 2); + } + + @Override + public void updateMeasureState(TextPaint tp) { + tp.baselineShift += (int) (tp.ascent() / 2); + } +} diff --git a/core/java/android/text/style/TabStopSpan.java b/core/java/android/text/style/TabStopSpan.java new file mode 100644 index 0000000..e5b7644 --- /dev/null +++ b/core/java/android/text/style/TabStopSpan.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2006 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; + +public interface TabStopSpan +extends ParagraphStyle +{ + public int getTabStop(); + + public static class Standard + implements TabStopSpan + { + public Standard(int where) { + mTab = where; + } + + public int getTabStop() { + return mTab; + } + + private int mTab; + } +} diff --git a/core/java/android/text/style/TextAppearanceSpan.java b/core/java/android/text/style/TextAppearanceSpan.java new file mode 100644 index 0000000..c4ec976 --- /dev/null +++ b/core/java/android/text/style/TextAppearanceSpan.java @@ -0,0 +1,198 @@ +/* + * Copyright (C) 2008 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.content.Context; +import android.content.res.ColorStateList; +import android.content.res.TypedArray; +import android.graphics.Paint; +import android.graphics.Typeface; +import android.text.TextPaint; + +/** + * Sets the text color, size, style, and typeface to match a TextAppearance + * resource. + */ +public class TextAppearanceSpan extends MetricAffectingSpan { + private String mTypeface; + private int mStyle; + private int mTextSize; + private ColorStateList mTextColor; + private ColorStateList mTextColorLink; + + /** + * Uses the specified TextAppearance resource to determine the + * text appearance. The <code>appearance</code> should be, for example, + * <code>android.R.style.TextAppearance_Small</code>. + */ + public TextAppearanceSpan(Context context, int appearance) { + this(context, appearance, -1); + } + + /** + * Uses the specified TextAppearance resource to determine the + * text appearance, and the specified text color resource + * to determine the color. The <code>appearance</code> should be, + * for example, <code>android.R.style.TextAppearance_Small</code>, + * and the <code>colorList</code> should be, for example, + * <code>android.R.styleable.Theme_textColorDim</code>. + */ + public TextAppearanceSpan(Context context, int appearance, + int colorList) { + TypedArray a = + context.obtainStyledAttributes(appearance, + com.android.internal.R.styleable.TextAppearance); + + mTextColor = a.getColorStateList(com.android.internal.R.styleable. + TextAppearance_textColor); + mTextColorLink = a.getColorStateList(com.android.internal.R.styleable. + TextAppearance_textColorLink); + mTextSize = a.getDimensionPixelSize(com.android.internal.R.styleable. + TextAppearance_textSize, -1); + + mStyle = a.getInt(com.android.internal.R.styleable.TextAppearance_textStyle, 0); + int tf = a.getInt(com.android.internal.R.styleable.TextAppearance_typeface, 0); + + switch (tf) { + case 1: + mTypeface = "sans"; + break; + + case 2: + mTypeface = "serif"; + break; + + case 3: + mTypeface = "monospace"; + break; + } + + a.recycle(); + + if (colorList >= 0) { + a = context.obtainStyledAttributes(com.android.internal.R.style.Theme, + com.android.internal.R.styleable.Theme); + + mTextColor = a.getColorStateList(colorList); + a.recycle(); + } + } + + /** + * Makes text be drawn with the specified typeface, size, style, + * and colors. + */ + public TextAppearanceSpan(String family, int style, int size, + ColorStateList color, ColorStateList linkColor) { + mTypeface = family; + mStyle = style; + mTextSize = size; + mTextColor = color; + mTextColorLink = linkColor; + } + + /** + * Returns the typeface family specified by this span, or <code>null</code> + * if it does not specify one. + */ + public String getFamily() { + return mTypeface; + } + + /** + * Returns the text color specified by this span, or <code>null</code> + * if it does not specify one. + */ + public ColorStateList getTextColor() { + return mTextColor; + } + + /** + * Returns the link color specified by this span, or <code>null</code> + * if it does not specify one. + */ + public ColorStateList getLinkTextColor() { + return mTextColorLink; + } + + /** + * Returns the text size specified by this span, or <code>-1</code> + * if it does not specify one. + */ + public int getTextSize() { + return mTextSize; + } + + /** + * Returns the text style specified by this span, or <code>0</code> + * if it does not specify one. + */ + public int getTextStyle() { + return mStyle; + } + + @Override + public void updateDrawState(TextPaint ds) { + updateMeasureState(ds); + + if (mTextColor != null) { + ds.setColor(mTextColor.getColorForState(ds.drawableState, 0)); + } + + if (mTextColorLink != null) { + ds.linkColor = mTextColor.getColorForState(ds.drawableState, 0); + } + } + + @Override + public void updateMeasureState(TextPaint ds) { + if (mTypeface != null || mStyle != 0) { + Typeface tf = ds.getTypeface(); + int style = 0; + + if (tf != null) { + style = tf.getStyle(); + } + + style |= mStyle; + + if (mTypeface != null) { + tf = Typeface.create(mTypeface, style); + } else if (tf == null) { + tf = Typeface.defaultFromStyle(style); + } else { + tf = Typeface.create(tf, style); + } + + int fake = style & ~tf.getStyle(); + + if ((fake & Typeface.BOLD) != 0) { + ds.setFakeBoldText(true); + } + + if ((fake & Typeface.ITALIC) != 0) { + ds.setTextSkewX(-0.25f); + } + + ds.setTypeface(tf); + } + + if (mTextSize > 0) { + ds.setTextSize(mTextSize); + } + } +} diff --git a/core/java/android/text/style/TypefaceSpan.java b/core/java/android/text/style/TypefaceSpan.java new file mode 100644 index 0000000..7519ac2 --- /dev/null +++ b/core/java/android/text/style/TypefaceSpan.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2006 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.graphics.Paint; +import android.graphics.Typeface; +import android.text.TextPaint; + +/** + * Changes the typeface family of the text to which the span is attached. + */ +public class TypefaceSpan extends MetricAffectingSpan { + private String mFamily; + + /** + * @param family The font family for this typeface. Examples include + * "monospace", "serif", and "sans-serif". + */ + public TypefaceSpan(String family) { + mFamily = family; + } + + /** + * Returns the font family name. + */ + public String getFamily() { + return mFamily; + } + + @Override + public void updateDrawState(TextPaint ds) { + apply(ds, mFamily); + } + + @Override + public void updateMeasureState(TextPaint paint) { + apply(paint, mFamily); + } + + private static void apply(Paint paint, String family) { + int oldStyle; + + Typeface old = paint.getTypeface(); + if (old == null) { + oldStyle = 0; + } else { + oldStyle = old.getStyle(); + } + + Typeface tf = Typeface.create(family, oldStyle); + int fake = oldStyle & ~tf.getStyle(); + + if ((fake & Typeface.BOLD) != 0) { + paint.setFakeBoldText(true); + } + + if ((fake & Typeface.ITALIC) != 0) { + paint.setTextSkewX(-0.25f); + } + + paint.setTypeface(tf); + } +} diff --git a/core/java/android/text/style/URLSpan.java b/core/java/android/text/style/URLSpan.java new file mode 100644 index 0000000..79809b5 --- /dev/null +++ b/core/java/android/text/style/URLSpan.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2006 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.content.Intent; +import android.net.Uri; +import android.text.TextPaint; +import android.view.View; + +public class URLSpan extends ClickableSpan { + + private String mURL; + + public URLSpan(String url) { + mURL = url; + } + + public String getURL() { + return mURL; + } + + @Override + public void onClick(View widget) { + Uri uri = Uri.parse(getURL()); + Intent intent = new Intent(Intent.ACTION_VIEW, uri); + intent.addCategory(Intent.CATEGORY_BROWSABLE); + widget.getContext().startActivity(intent); + } +} diff --git a/core/java/android/text/style/UnderlineSpan.java b/core/java/android/text/style/UnderlineSpan.java new file mode 100644 index 0000000..5dce0f2 --- /dev/null +++ b/core/java/android/text/style/UnderlineSpan.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2006 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.text.TextPaint; + +public class UnderlineSpan extends CharacterStyle { + + @Override + public void updateDrawState(TextPaint ds) { + ds.setUnderlineText(true); + } +} diff --git a/core/java/android/text/style/UpdateLayout.java b/core/java/android/text/style/UpdateLayout.java new file mode 100644 index 0000000..211685a --- /dev/null +++ b/core/java/android/text/style/UpdateLayout.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2006 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; + +/** + * The classes that affect character-level text formatting in a way that + * triggers a text layout update when one is added or remove must implement + * this interface. + */ +public interface UpdateLayout { } diff --git a/core/java/android/text/style/WrapTogetherSpan.java b/core/java/android/text/style/WrapTogetherSpan.java new file mode 100644 index 0000000..11721a8 --- /dev/null +++ b/core/java/android/text/style/WrapTogetherSpan.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2006 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; + +public interface WrapTogetherSpan +extends ParagraphStyle +{ + +} diff --git a/core/java/android/text/style/package.html b/core/java/android/text/style/package.html new file mode 100644 index 0000000..0a8520c --- /dev/null +++ b/core/java/android/text/style/package.html @@ -0,0 +1,10 @@ +<html> +<body> + +<p>Provides classes used to view or change the style of a span of text in a View object. +The classes with a subclass Standard are passed in to {@link android.text.SpannableString#setSpan(java.lang.Object, int, int, int) +SpannableString.setSpan()} or {@link android.text.SpannableStringBuilder#setSpan(java.lang.Object, int, int, int) +SpannableStringBuilder.setSpan()} to add a new styled span to a string in a View object. + +</body> +</html> diff --git a/core/java/android/text/util/Linkify.java b/core/java/android/text/util/Linkify.java new file mode 100644 index 0000000..79ecfbd --- /dev/null +++ b/core/java/android/text/util/Linkify.java @@ -0,0 +1,533 @@ +/* + * Copyright (C) 2007 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.util; + +import android.text.method.LinkMovementMethod; +import android.text.method.MovementMethod; +import android.text.style.URLSpan; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.Spanned; +import android.webkit.WebView; +import android.widget.TextView; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Linkify take a piece of text and a regular expression and turns all of the + * regex matches in the text into clickable links. This is particularly + * useful for matching things like email addresses, web urls, etc. and making + * them actionable. + * + * Alone with the pattern that is to be matched, a url scheme prefix is also + * required. Any pattern match that does not begin with the supplied scheme + * will have the scheme prepended to the matched text when the clickable url + * is created. For instance, if you are matching web urls you would supply + * the scheme <code>http://</code>. If the pattern matches example.com, which + * does not have a url scheme prefix, the supplied scheme will be prepended to + * create <code>http://example.com</code> when the clickable url link is + * created. + */ + +public class Linkify { + /** + * Bit field indicating that web URLs should be matched in methods that + * take an options mask + */ + public static final int WEB_URLS = 0x01; + + /** + * Bit field indicating that email addresses should be matched in methods + * that take an options mask + */ + public static final int EMAIL_ADDRESSES = 0x02; + + /** + * Bit field indicating that phone numbers should be matched in methods that + * take an options mask + */ + public static final int PHONE_NUMBERS = 0x04; + + /** + * Bit field indicating that phone numbers should be matched in methods that + * take an options mask + */ + public static final int MAP_ADDRESSES = 0x08; + + /** + * Bit mask indicating that all available patterns should be matched in + * methods that take an options mask + */ + public static final int ALL = WEB_URLS | EMAIL_ADDRESSES | PHONE_NUMBERS + | MAP_ADDRESSES; + + /** + * Don't treat anything with fewer than this many digits as a + * phone number. + */ + private static final int PHONE_NUMBER_MINIMUM_DIGITS = 5; + + /** + * Filters out web URL matches that occur after an at-sign (@). This is + * to prevent turning the domain name in an email address into a web link. + */ + public static final MatchFilter sUrlMatchFilter = new MatchFilter() { + public final boolean acceptMatch(CharSequence s, int start, int end) { + if (start == 0) { + return true; + } + + if (s.charAt(start - 1) == '@') { + return false; + } + + return true; + } + }; + + /** + * Filters out URL matches that don't have enough digits to be a + * phone number. + */ + public static final MatchFilter sPhoneNumberMatchFilter = + new MatchFilter() { + public final boolean acceptMatch(CharSequence s, int start, int end) { + int digitCount = 0; + + for (int i = start; i < end; i++) { + if (Character.isDigit(s.charAt(i))) { + digitCount++; + if (digitCount >= PHONE_NUMBER_MINIMUM_DIGITS) { + return true; + } + } + } + return false; + } + }; + + /** + * Transforms matched phone number text into something suitable + * to be used in a tel: URL. It does this by removing everything + * but the digits and plus signs. For instance: + * '+1 (919) 555-1212' + * becomes '+19195551212' + */ + public static final TransformFilter sPhoneNumberTransformFilter = + new TransformFilter() { + public final String transformUrl(final Matcher match, String url) { + return Regex.digitsAndPlusOnly(match); + } + }; + + /** + * MatchFilter enables client code to have more control over + * what is allowed to match and become a link, and what is not. + * + * For example: when matching web urls you would like things like + * http://www.example.com to match, as well as just example.com itelf. + * However, you would not want to match against the domain in + * support@example.com. So, when matching against a web url pattern you + * might also include a MatchFilter that disallows the match if it is + * immediately preceded by an at-sign (@). + */ + public interface MatchFilter { + /** + * Examines the character span matched by the pattern and determines + * if the match should be turned into an actionable link. + * + * @param s The body of text against which the pattern + * was matched + * @param start The index of the first character in s that was + * matched by the pattern - inclusive + * @param end The index of the last character in s that was + * matched - exclusive + * + * @return Whether this match should be turned into a link + */ + boolean acceptMatch(CharSequence s, int start, int end); + } + + /** + * TransformFilter enables client code to have more control over + * how matched patterns are represented as URLs. + * + * For example: when converting a phone number such as (919) 555-1212 + * into a tel: URL the parentheses, white space, and hyphen need to be + * removed to produce tel:9195551212. + */ + public interface TransformFilter { + /** + * Examines the matched text and either passes it through or uses the + * data in the Matcher state to produce a replacement. + * + * @param match The regex matcher state that found this URL text + * @param url The text that was matched + * + * @return The transformed form of the URL + */ + String transformUrl(final Matcher match, String url); + } + + /** + * Scans the text of the provided Spannable and turns all occurrences + * of the link types indicated in the mask into clickable links. + * If the mask is nonzero, it also removes any existing URLSpans + * attached to the Spannable, to avoid problems if you call it + * repeatedly on the same text. + */ + public static final boolean addLinks(Spannable text, int mask) { + if (mask == 0) { + return false; + } + + URLSpan[] old = text.getSpans(0, text.length(), URLSpan.class); + + for (int i = old.length - 1; i >= 0; i--) { + text.removeSpan(old[i]); + } + + ArrayList<LinkSpec> links = new ArrayList<LinkSpec>(); + + if ((mask & WEB_URLS) != 0) { + gatherLinks(links, text, Regex.WEB_URL_PATTERN, + new String[] { "http://", "https://" }, + sUrlMatchFilter, null); + } + + if ((mask & EMAIL_ADDRESSES) != 0) { + gatherLinks(links, text, Regex.EMAIL_ADDRESS_PATTERN, + new String[] { "mailto:" }, + null, null); + } + + if ((mask & PHONE_NUMBERS) != 0) { + gatherLinks(links, text, Regex.PHONE_PATTERN, + new String[] { "tel:" }, + sPhoneNumberMatchFilter, sPhoneNumberTransformFilter); + } + + if ((mask & MAP_ADDRESSES) != 0) { + gatherMapLinks(links, text); + } + + pruneOverlaps(links); + + if (links.size() == 0) { + return false; + } + + for (LinkSpec link: links) { + applyLink(link.url, link.start, link.end, text); + } + + return true; + } + + /** + * Scans the text of the provided TextView and turns all occurrences of + * the link types indicated in the mask into clickable links. If matches + * are found the movement method for the TextView is set to + * LinkMovementMethod. + */ + public static final boolean addLinks(TextView text, int mask) { + if (mask == 0) { + return false; + } + + CharSequence t = text.getText(); + + if (t instanceof Spannable) { + if (addLinks((Spannable) t, mask)) { + addLinkMovementMethod(text); + return true; + } + + return false; + } else { + SpannableString s = SpannableString.valueOf(t); + + if (addLinks(s, mask)) { + addLinkMovementMethod(text); + text.setText(s); + + return true; + } + + return false; + } + } + + private static final void addLinkMovementMethod(TextView t) { + MovementMethod m = t.getMovementMethod(); + + if ((m == null) || !(m instanceof LinkMovementMethod)) { + if (t.getLinksClickable()) { + t.setMovementMethod(LinkMovementMethod.getInstance()); + } + } + } + + /** + * Applies a regex to the text of a TextView turning the matches into + * links. If links are found then UrlSpans are applied to the link + * text match areas, and the movement method for the text is changed + * to LinkMovementMethod. + * + * @param text TextView whose text is to be marked-up with links + * @param pattern Regex pattern to be used for finding links + * @param scheme Url scheme string (eg <code>http://</code> to be + * prepended to the url of links that do not have + * a scheme specified in the link text + */ + public static final void addLinks(TextView text, Pattern pattern, + String scheme) { + addLinks(text, pattern, scheme, null, null); + } + + /** + * Applies a regex to the text of a TextView turning the matches into + * links. If links are found then UrlSpans are applied to the link + * text match areas, and the movement method for the text is changed + * to LinkMovementMethod. + * + * @param text TextView whose text is to be marked-up with links + * @param p Regex pattern to be used for finding links + * @param scheme Url scheme string (eg <code>http://</code> to be + * prepended to the url of links that do not have + * a scheme specified in the link text + * @param matchFilter The filter that is used to allow the client code + * additional control over which pattern matches are + * to be converted into links. + */ + public static final void addLinks(TextView text, Pattern p, String scheme, + MatchFilter matchFilter, TransformFilter transformFilter) { + SpannableString s = SpannableString.valueOf(text.getText()); + + if (addLinks(s, p, scheme, matchFilter, transformFilter)) { + text.setText(s); + addLinkMovementMethod(text); + } + } + + /** + * Applies a regex to a Spannable turning the matches into + * links. + * + * @param text Spannable whose text is to be marked-up with + * links + * @param pattern Regex pattern to be used for finding links + * @param scheme Url scheme string (eg <code>http://</code> to be + * prepended to the url of links that do not have + * a scheme specified in the link text + */ + public static final boolean addLinks(Spannable text, Pattern pattern, + String scheme) { + return addLinks(text, pattern, scheme, null, null); + } + + /** + * Applies a regex to a Spannable turning the matches into + * links. + * + * @param s Spannable whose text is to be marked-up with + * links + * @param p Regex pattern to be used for finding links + * @param scheme Url scheme string (eg <code>http://</code> to be + * prepended to the url of links that do not have + * a scheme specified in the link text + * @param matchFilter The filter that is used to allow the client code + * additional control over which pattern matches are + * to be converted into links. + */ + public static final boolean addLinks(Spannable s, Pattern p, + String scheme, MatchFilter matchFilter, + TransformFilter transformFilter) { + boolean hasMatches = false; + String prefix = (scheme == null) ? "" : scheme.toLowerCase(); + Matcher m = p.matcher(s); + + while (m.find()) { + int start = m.start(); + int end = m.end(); + boolean allowed = true; + + if (matchFilter != null) { + allowed = matchFilter.acceptMatch(s, start, end); + } + + if (allowed) { + String url = makeUrl(m.group(0), new String[] { prefix }, + m, transformFilter); + + applyLink(url, start, end, s); + hasMatches = true; + } + } + + return hasMatches; + } + + private static final void applyLink(String url, int start, int end, + Spannable text) { + URLSpan span = new URLSpan(url); + + text.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + + private static final String makeUrl(String url, String[] prefixes, + Matcher m, TransformFilter filter) { + if (filter != null) { + url = filter.transformUrl(m, url); + } + + boolean hasPrefix = false; + for (int i = 0; i < prefixes.length; i++) { + if (url.regionMatches(true, 0, prefixes[i], 0, + prefixes[i].length())) { + hasPrefix = true; + break; + } + } + if (!hasPrefix) { + url = prefixes[0] + url; + } + + return url; + } + + private static final void gatherLinks(ArrayList<LinkSpec> links, + Spannable s, Pattern pattern, String[] schemes, + MatchFilter matchFilter, TransformFilter transformFilter) { + Matcher m = pattern.matcher(s); + + while (m.find()) { + int start = m.start(); + int end = m.end(); + + if (matchFilter == null || matchFilter.acceptMatch(s, start, end)) { + LinkSpec spec = new LinkSpec(); + String url = makeUrl(m.group(0), schemes, m, transformFilter); + + spec.url = url; + spec.start = start; + spec.end = end; + + links.add(spec); + } + } + } + + private static final void gatherMapLinks(ArrayList<LinkSpec> links, + Spannable s) { + String string = s.toString(); + String address; + int base = 0; + while ((address = WebView.findAddress(string)) != null) { + int start = string.indexOf(address); + if (start < 0) { + break; + } + LinkSpec spec = new LinkSpec(); + int length = address.length(); + int end = start + length; + spec.start = base + start; + spec.end = base + end; + string = string.substring(end); + base += end; + + String encodedAddress = null; + try { + encodedAddress = URLEncoder.encode(address,"UTF-8"); + } catch (UnsupportedEncodingException e) { + continue; + } + spec.url = "geo:0,0?q=" + encodedAddress; + links.add(spec); + } + } + + private static final void pruneOverlaps(ArrayList<LinkSpec> links) { + Comparator<LinkSpec> c = new Comparator<LinkSpec>() { + public final int compare(LinkSpec a, LinkSpec b) { + if (a.start < b.start) { + return -1; + } + + if (a.start > b.start) { + return 1; + } + + if (a.end < b.end) { + return 1; + } + + if (a.end > b.end) { + return -1; + } + + return 0; + } + + public final boolean equals(Object o) { + return false; + } + }; + + Collections.sort(links, c); + + int len = links.size(); + int i = 0; + + while (i < len - 1) { + LinkSpec a = links.get(i); + LinkSpec b = links.get(i + 1); + int remove = -1; + + if ((a.start <= b.start) && (a.end > b.start)) { + if (b.end <= a.end) { + remove = i + 1; + } else if ((a.end - a.start) > (b.end - b.start)) { + remove = i + 1; + } else if ((a.end - a.start) < (b.end - b.start)) { + remove = i; + } + + if (remove != -1) { + links.remove(remove); + len--; + continue; + } + + } + + i++; + } + } +} + +class LinkSpec { + String url; + int start; + int end; +} diff --git a/core/java/android/text/util/Regex.java b/core/java/android/text/util/Regex.java new file mode 100644 index 0000000..55ad140 --- /dev/null +++ b/core/java/android/text/util/Regex.java @@ -0,0 +1,192 @@ +/* + * Copyright (C) 2007 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.util; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * @hide + */ +public class Regex { + /** + * Regular expression pattern to match all IANA top-level domains. + * List accurate as of 2007/06/15. List taken from: + * http://data.iana.org/TLD/tlds-alpha-by-domain.txt + * This pattern is auto-generated by //device/tools/make-iana-tld-pattern.py + */ + public static final Pattern TOP_LEVEL_DOMAIN_PATTERN + = Pattern.compile( + "((aero|arpa|asia|a[cdefgilmnoqrstuwxz])" + + "|(biz|b[abdefghijmnorstvwyz])" + + "|(cat|com|coop|c[acdfghiklmnoruvxyz])" + + "|d[ejkmoz]" + + "|(edu|e[cegrstu])" + + "|f[ijkmor]" + + "|(gov|g[abdefghilmnpqrstuwy])" + + "|h[kmnrtu]" + + "|(info|int|i[delmnoqrst])" + + "|(jobs|j[emop])" + + "|k[eghimnrwyz]" + + "|l[abcikrstuvy]" + + "|(mil|mobi|museum|m[acdghklmnopqrstuvwxyz])" + + "|(name|net|n[acefgilopruz])" + + "|(org|om)" + + "|(pro|p[aefghklmnrstwy])" + + "|qa" + + "|r[eouw]" + + "|s[abcdeghijklmnortuvyz]" + + "|(tel|travel|t[cdfghjklmnoprtvwz])" + + "|u[agkmsyz]" + + "|v[aceginu]" + + "|w[fs]" + + "|y[etu]" + + "|z[amw])"); + + /** + * Regular expression pattern to match RFC 1738 URLs + * List accurate as of 2007/06/15. List taken from: + * http://data.iana.org/TLD/tlds-alpha-by-domain.txt + * This pattern is auto-generated by //device/tools/make-iana-tld-pattern.py + */ + public static final Pattern WEB_URL_PATTERN + = Pattern.compile( + "((?:(http|https):\\/\\/(?:(?:[a-zA-Z0-9\\$\\-\\_\\.\\+\\!\\*\\'\\(\\)" + + "\\,\\;\\?\\&\\=]|(?:\\%[a-fA-F0-9]{2}))+(?:\\:(?:[a-zA-Z0-9\\$\\-\\_" + + "\\.\\+\\!\\*\\'\\(\\)\\,\\;\\?\\&\\=]|(?:\\%[a-fA-F0-9]{2}))+)?\\@)?)?" + + "((?:(?:[a-zA-Z0-9][a-zA-Z0-9\\-]*\\.)+" // named host + + "(?:" // plus top level domain + + "(?:aero|arpa|asia|a[cdefgilmnoqrstuwxz])" + + "|(?:biz|b[abdefghijmnorstvwyz])" + + "|(?:cat|com|coop|c[acdfghiklmnoruvxyz])" + + "|d[ejkmoz]" + + "|(?:edu|e[cegrstu])" + + "|f[ijkmor]" + + "|(?:gov|g[abdefghilmnpqrstuwy])" + + "|h[kmnrtu]" + + "|(?:info|int|i[delmnoqrst])" + + "|(?:jobs|j[emop])" + + "|k[eghimnrwyz]" + + "|l[abcikrstuvy]" + + "|(?:mil|mobi|museum|m[acdghklmnopqrstuvwxyz])" + + "|(?:name|net|n[acefgilopruz])" + + "|(?:org|om)" + + "|(?:pro|p[aefghklmnrstwy])" + + "|qa" + + "|r[eouw]" + + "|s[abcdeghijklmnortuvyz]" + + "|(?:tel|travel|t[cdfghjklmnoprtvwz])" + + "|u[agkmsyz]" + + "|v[aceginu]" + + "|w[fs]" + + "|y[etu]" + + "|z[amw]))" + + "|(?:(?:25[0-5]|2[0-4]" // or ip address + + "[0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9])\\.(?:25[0-5]|2[0-4][0-9]" + + "|[0-1][0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(?:25[0-5]|2[0-4][0-9]|[0-1]" + + "[0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(?:25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}" + + "|[1-9][0-9]|[0-9])))" + + "(?:\\:\\d{1,5})?)" // plus option port number + + "(\\/(?:(?:[a-zA-Z0-9\\;\\/\\?\\:\\@\\&\\=\\#\\~" // plus option query params + + "\\-\\.\\+\\!\\*\\'\\(\\)\\,\\_])|(?:\\%[a-fA-F0-9]{2}))*)?" + + "\\b"); // and finally, a word boundary this is to stop foo.sure from matching as foo.su + + public static final Pattern IP_ADDRESS_PATTERN + = Pattern.compile( + "((25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9])\\.(25[0-5]|2[0-4]" + + "[0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1]" + + "[0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}" + + "|[1-9][0-9]|[0-9]))"); + + public static final Pattern DOMAIN_NAME_PATTERN + = Pattern.compile( + "(((([a-zA-Z0-9][a-zA-Z0-9\\-]*)*[a-zA-Z0-9]\\.)+" + + TOP_LEVEL_DOMAIN_PATTERN + ")|" + + IP_ADDRESS_PATTERN + ")"); + + public static final Pattern EMAIL_ADDRESS_PATTERN + = Pattern.compile( + "[a-zA-Z0-9\\+\\.\\_\\%\\-]+" + + "\\@" + + "[a-zA-Z0-9][a-zA-Z0-9\\-]*" + + "(" + + "\\." + + "[a-zA-Z0-9][a-zA-Z0-9\\-]*" + + ")+" + ); + + /** + * This pattern is intended for searching for things that look like they + * might be phone numbers in arbitrary text, not for validating whether + * something is in fact a phone number. It will miss many things that + * are legitimate phone numbers. + */ + public static final Pattern PHONE_PATTERN + = Pattern.compile( + "(?:\\+[0-9]+)|(?:[0-9()][0-9()\\- \\.][0-9()\\- \\.]+[0-9])"); + + /** + * Convenience method to take all of the non-null matching groups in a + * regex Matcher and return them as a concatenated string. + * + * @param matcher The Matcher object from which grouped text will + * be extracted + * + * @return A String comprising all of the non-null matched + * groups concatenated together + */ + public static final String concatGroups(Matcher matcher) { + StringBuilder b = new StringBuilder(); + final int numGroups = matcher.groupCount(); + + for (int i = 1; i <= numGroups; i++) { + String s = matcher.group(i); + + System.err.println("Group(" + i + ") : " + s); + + if (s != null) { + b.append(s); + } + } + + return b.toString(); + } + + /** + * Convenience method to return only the digits and plus signs + * in the matching string. + * + * @param matcher The Matcher object from which digits and plus will + * be extracted + * + * @return A String comprising all of the digits and plus in + * the match + */ + public static final String digitsAndPlusOnly(Matcher matcher) { + StringBuilder buffer = new StringBuilder(); + String matchingRegion = matcher.group(); + + for (int i = 0, size = matchingRegion.length(); i < size; i++) { + char character = matchingRegion.charAt(i); + + if (character == '+' || Character.isDigit(character)) { + buffer.append(character); + } + } + return buffer.toString(); + } +} diff --git a/core/java/android/text/util/Rfc822Token.java b/core/java/android/text/util/Rfc822Token.java new file mode 100644 index 0000000..e6472df --- /dev/null +++ b/core/java/android/text/util/Rfc822Token.java @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2008 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.util; + +/** + * This class stores an RFC 822-like name, address, and comment, + * and provides methods to convert them to quoted strings. + */ +public class Rfc822Token { + private String mName, mAddress, mComment; + + /** + * Creates a new Rfc822Token with the specified name, address, + * and comment. + */ + public Rfc822Token(String name, String address, String comment) { + mName = name; + mAddress = address; + mComment = comment; + } + + /** + * Returns the name part. + */ + public String getName() { + return mName; + } + + /** + * Returns the address part. + */ + public String getAddress() { + return mAddress; + } + + /** + * Returns the comment part. + */ + public String getComment() { + return mComment; + } + + /** + * Changes the name to the specified name. + */ + public void setName(String name) { + mName = name; + } + + /** + * Changes the address to the specified address. + */ + public void setAddress(String address) { + mAddress = address; + } + + /** + * Changes the comment to the specified comment. + */ + public void setComment(String comment) { + mComment = comment; + } + + /** + * Returns the name (with quoting added if necessary), + * the comment (in parentheses), and the address (in angle brackets). + * This should be suitable for inclusion in an RFC 822 address list. + */ + public String toString() { + StringBuilder sb = new StringBuilder(); + + if (mName != null && mName.length() != 0) { + sb.append(quoteNameIfNecessary(mName)); + sb.append(' '); + } + + if (mComment != null && mComment.length() != 0) { + sb.append('('); + sb.append(quoteComment(mComment)); + sb.append(") "); + } + + if (mAddress != null && mAddress.length() != 0) { + sb.append('<'); + sb.append(mAddress); + sb.append('>'); + } + + return sb.toString(); + } + + /** + * Returns the name, conservatively quoting it if there are any + * characters that are likely to cause trouble outside of a + * quoted string, or returning it literally if it seems safe. + */ + public static String quoteNameIfNecessary(String name) { + int len = name.length(); + + for (int i = 0; i < len; i++) { + char c = name.charAt(i); + + if (! ((c >= 'A' && i <= 'Z') || + (c >= 'a' && c <= 'z') || + (c == ' ') || + (c >= '0' && c <= '9'))) { + return '"' + quoteName(name) + '"'; + } + } + + return name; + } + + /** + * Returns the name, with internal backslashes and quotation marks + * preceded by backslashes. The outer quote marks themselves are not + * added by this method. + */ + public static String quoteName(String name) { + StringBuilder sb = new StringBuilder(); + + int len = name.length(); + for (int i = 0; i < len; i++) { + char c = name.charAt(i); + + if (c == '\\' || c == '"') { + sb.append('\\'); + } + + sb.append(c); + } + + return sb.toString(); + } + + /** + * Returns the comment, with internal backslashes and parentheses + * preceded by backslashes. The outer parentheses themselves are + * not added by this method. + */ + public static String quoteComment(String comment) { + int len = comment.length(); + StringBuilder sb = new StringBuilder(); + + for (int i = 0; i < len; i++) { + char c = comment.charAt(i); + + if (c == '(' || c == ')' || c == '\\') { + sb.append('\\'); + } + + sb.append(c); + } + + return sb.toString(); + } +} + diff --git a/core/java/android/text/util/Rfc822Tokenizer.java b/core/java/android/text/util/Rfc822Tokenizer.java new file mode 100644 index 0000000..d4e78b0 --- /dev/null +++ b/core/java/android/text/util/Rfc822Tokenizer.java @@ -0,0 +1,292 @@ +/* + * Copyright (C) 2008 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.util; + +import android.widget.MultiAutoCompleteTextView; + +import java.util.ArrayList; + +/** + * This class works as a Tokenizer for MultiAutoCompleteTextView for + * address list fields, and also provides a method for converting + * a string of addresses (such as might be typed into such a field) + * into a series of Rfc822Tokens. + */ +public class Rfc822Tokenizer implements MultiAutoCompleteTextView.Tokenizer { + /** + * This constructor will try to take a string like + * "Foo Bar (something) <foo\@google.com>, + * blah\@google.com (something)" + * and convert it into one or more Rfc822Tokens. + * It does *not* decode MIME encoded-words; charset conversion + * must already have taken place if necessary. + * It will try to be tolerant of broken syntax instead of + * returning an error. + */ + public static Rfc822Token[] tokenize(CharSequence text) { + ArrayList<Rfc822Token> out = new ArrayList<Rfc822Token>(); + StringBuilder name = new StringBuilder(); + StringBuilder address = new StringBuilder(); + StringBuilder comment = new StringBuilder(); + + int i = 0; + int cursor = text.length(); + + while (i < cursor) { + char c = text.charAt(i); + + if (c == ',' || c == ';') { + i++; + + while (i < cursor && text.charAt(i) == ' ') { + i++; + } + + crunch(name); + + if (address.length() > 0) { + out.add(new Rfc822Token(name.toString(), + address.toString(), + comment.toString())); + } else if (name.length() > 0) { + out.add(new Rfc822Token(null, + name.toString(), + comment.toString())); + } + + name.setLength(0); + address.setLength(0); + address.setLength(0); + } else if (c == '"') { + i++; + + while (i < cursor) { + c = text.charAt(i); + + if (c == '"') { + i++; + break; + } else if (c == '\\') { + name.append(text.charAt(i + 1)); + i += 2; + } else { + name.append(c); + i++; + } + } + } else if (c == '(') { + int level = 1; + i++; + + while (i < cursor && level > 0) { + c = text.charAt(i); + + if (c == ')') { + if (level > 1) { + comment.append(c); + } + + level--; + i++; + } else if (c == '(') { + comment.append(c); + level++; + i++; + } else if (c == '\\') { + comment.append(text.charAt(i + 1)); + i += 2; + } else { + comment.append(c); + i++; + } + } + } else if (c == '<') { + i++; + + while (i < cursor) { + c = text.charAt(i); + + if (c == '>') { + i++; + break; + } else { + address.append(c); + i++; + } + } + } else if (c == ' ') { + name.append('\0'); + i++; + } else { + name.append(c); + i++; + } + } + + crunch(name); + + if (address.length() > 0) { + out.add(new Rfc822Token(name.toString(), + address.toString(), + comment.toString())); + } else if (name.length() > 0) { + out.add(new Rfc822Token(null, + name.toString(), + comment.toString())); + } + + return out.toArray(new Rfc822Token[out.size()]); + } + + private static void crunch(StringBuilder sb) { + int i = 0; + int len = sb.length(); + + while (i < len) { + char c = sb.charAt(i); + + if (c == '\0') { + if (i == 0 || i == len - 1 || + sb.charAt(i - 1) == ' ' || + sb.charAt(i - 1) == '\0' || + sb.charAt(i + 1) == ' ' || + sb.charAt(i + 1) == '\0') { + sb.deleteCharAt(i); + len--; + } else { + i++; + } + } else { + i++; + } + } + + for (i = 0; i < len; i++) { + if (sb.charAt(i) == '\0') { + sb.setCharAt(i, ' '); + } + } + } + + /** + * {@inheritDoc} + */ + public int findTokenStart(CharSequence text, int cursor) { + /* + * It's hard to search backward, so search forward until + * we reach the cursor. + */ + + int best = 0; + int i = 0; + + while (i < cursor) { + i = findTokenEnd(text, i); + + if (i < cursor) { + i++; // Skip terminating punctuation + + while (i < cursor && text.charAt(i) == ' ') { + i++; + } + + if (i < cursor) { + best = i; + } + } + } + + return best; + } + + /** + * {@inheritDoc} + */ + public int findTokenEnd(CharSequence text, int cursor) { + int len = text.length(); + int i = cursor; + + while (i < len) { + char c = text.charAt(i); + + if (c == ',' || c == ';') { + return i; + } else if (c == '"') { + i++; + + while (i < len) { + c = text.charAt(i); + + if (c == '"') { + i++; + break; + } else if (c == '\\') { + i += 2; + } else { + i++; + } + } + } else if (c == '(') { + int level = 1; + i++; + + while (i < len && level > 0) { + c = text.charAt(i); + + if (c == ')') { + level--; + i++; + } else if (c == '(') { + level++; + i++; + } else if (c == '\\') { + i += 2; + } else { + i++; + } + } + } else if (c == '<') { + i++; + + while (i < len) { + c = text.charAt(i); + + if (c == '>') { + i++; + break; + } else { + i++; + } + } + } else { + i++; + } + } + + return i; + } + + /** + * Terminates the specified address with a comma and space. + * This assumes that the specified text already has valid syntax. + * The Adapter subclass's convertToString() method must make that + * guarantee. + */ + public CharSequence terminateToken(CharSequence text) { + return text + ", "; + } +} diff --git a/core/java/android/text/util/Rfc822Validator.java b/core/java/android/text/util/Rfc822Validator.java new file mode 100644 index 0000000..9f03bb0 --- /dev/null +++ b/core/java/android/text/util/Rfc822Validator.java @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2008 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.util; + +import android.widget.AutoCompleteTextView; + +import java.util.regex.Pattern; + +/** + * This class works as a Validator for AutoCompleteTextView for + * email addresses. If a token does not appear to be a valid address, + * it is trimmed of characters that cannot legitimately appear in one + * and has the specified domain name added. It is meant for use with + * {@link Rfc822Token} and {@link Rfc822Tokenizer}. + * + * @deprecated In the future make sure we don't quietly alter the user's + * text in ways they did not intend. Meanwhile, hide this + * class from the public API because it does not even have + * a full understanding of the syntax it claims to correct. + * @hide + */ +public class Rfc822Validator implements AutoCompleteTextView.Validator { + /* + * Regex.EMAIL_ADDRESS_PATTERN hardcodes the TLD that we accept, but we + * want to make sure we will keep accepting email addresses with TLD's + * that don't exist at the time of this writing, so this regexp relaxes + * that constraint by accepting any kind of top level domain, not just + * ".com", ".fr", etc... + */ + private static final Pattern EMAIL_ADDRESS_PATTERN = + Pattern.compile("[^\\s@]+@[^\\s@]+\\.[a-zA-z][a-zA-Z][a-zA-Z]*"); + + private String mDomain; + + /** + * Constructs a new validator that uses the specified domain name as + * the default when none is specified. + */ + public Rfc822Validator(String domain) { + mDomain = domain; + } + + /** + * {@inheritDoc} + */ + public boolean isValid(CharSequence text) { + Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(text); + + return tokens.length == 1 && + EMAIL_ADDRESS_PATTERN. + matcher(tokens[0].getAddress()).matches(); + } + + /** + * @return a string in which all the characters that are illegal for the username + * part of the email address have been removed. + */ + private String removeIllegalCharacters(String s) { + StringBuilder result = new StringBuilder(); + int length = s.length(); + for (int i = 0; i < length; i++) { + char c = s.charAt(i); + + /* + * An RFC822 atom can contain any ASCII printing character + * except for periods and any of the following punctuation. + * A local-part can contain multiple atoms, concatenated by + * periods, so do allow periods here. + */ + + if (c <= ' ' || c > '~') { + continue; + } + + if (c == '(' || c == ')' || c == '<' || c == '>' || + c == '@' || c == ',' || c == ';' || c == ':' || + c == '\\' || c == '"' || c == '[' || c == ']') { + continue; + } + + result.append(c); + } + return result.toString(); + } + + /** + * {@inheritDoc} + */ + public CharSequence fixText(CharSequence cs) { + Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(cs); + StringBuilder sb = new StringBuilder(); + + for (int i = 0; i < tokens.length; i++) { + String text = tokens[i].getAddress(); + int index = text.indexOf('@'); + if (index < 0) { + // If there is no @, just append the domain of the account + tokens[i].setAddress(removeIllegalCharacters(text) + "@" + mDomain); + } else { + // Otherwise, remove everything right of the '@' and append the domain + // ("a@b" becomes "a@gmail.com"). + String fix = removeIllegalCharacters(text.substring(0, index)); + tokens[i].setAddress(fix + "@" + mDomain); + } + + sb.append(tokens[i].toString()); + if (i + 1 < tokens.length) { + sb.append(", "); + } + } + + return sb; + } +} diff --git a/core/java/android/text/util/package.html b/core/java/android/text/util/package.html new file mode 100644 index 0000000..d9312aa2 --- /dev/null +++ b/core/java/android/text/util/package.html @@ -0,0 +1,6 @@ +<HTML> +<BODY> +Utilities for converting identifiable text strings into clickable links +and creating RFC 822-type message (SMTP) tokens. +</BODY> +</HTML>
\ No newline at end of file |