Overhaul Calculator UI.

Bug: 14418545
Bug: 14419084
Bug: 14419142
Bug: 14420277
Bug: 14466652
Bug: 14564559
Bug: 14564608
Bug: 14846724
Bug: 15090154
Bug: 15287699
Bug: 15289526
Bug: 15289616
Change-Id: I93e1530446d5bd6a4c3189f751c88ece1abc7767
diff --git a/src/com/android/calculator2/CalculatorEditText.java b/src/com/android/calculator2/CalculatorEditText.java
index b40bb1e..e31d571 100644
--- a/src/com/android/calculator2/CalculatorEditText.java
+++ b/src/com/android/calculator2/CalculatorEditText.java
@@ -1,11 +1,11 @@
 /*
- * Copyright (C) 2010 The Android Open Source Project
+ * Copyright (C) 2014 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
+ *   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,
@@ -16,290 +16,24 @@
 
 package com.android.calculator2;
 
-import android.content.ClipData;
-import android.content.ClipboardManager;
 import android.content.Context;
-import android.content.res.Resources;
+import android.content.res.TypedArray;
 import android.graphics.Paint;
-import android.text.Editable;
-import android.text.InputType;
-import android.text.TextUtils;
+import android.graphics.Paint.FontMetricsInt;
+import android.graphics.Rect;
+import android.text.method.ScrollingMovementMethod;
+import android.text.TextPaint;
 import android.util.AttributeSet;
-import android.util.Log;
 import android.util.TypedValue;
 import android.view.ActionMode;
-import android.view.ContextMenu;
 import android.view.Menu;
 import android.view.MenuItem;
 import android.view.MotionEvent;
-import android.view.accessibility.AccessibilityEvent;
-import android.view.accessibility.AccessibilityNodeInfo;
 import android.widget.EditText;
-import android.widget.Toast;
-
-import com.android.calculator2.R;
-
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
 
 public class CalculatorEditText extends EditText {
-    private static final String LOG_TAG = "Calculator2";
-    private static final int CUT = 0;
-    private static final int COPY = 1;
-    private static final int PASTE = 2;
 
-    private static Map<String, String> sReplacementTable;
-    private static String[] sOperators;
-
-    private final int mMaximumTextSize;
-    private final int mMinimumTextSize;
-    private final int mStepTextSize;
-
-    private int mWidthConstraint = -1;
-
-    private String[] mMenuItemsStrings;
-
-    public CalculatorEditText(Context context) {
-        this(context, null);
-    }
-
-    public CalculatorEditText(Context context, AttributeSet attrs) {
-        super(context, attrs);
-
-        setCustomSelectionActionModeCallback(new NoTextSelectionMode());
-        setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
-        setCursorVisible(false);
-
-        final Resources res = getResources();
-        mMaximumTextSize = res.getDimensionPixelSize(R.dimen.display_maximum_text_size);
-        mMinimumTextSize = res.getDimensionPixelSize(R.dimen.display_minimum_text_size);
-        mStepTextSize = res.getDimensionPixelSize(R.dimen.display_step_text_size);
-    }
-
-    @Override
-    public boolean onTouchEvent(MotionEvent event) {
-        if (event.getActionMasked() == MotionEvent.ACTION_UP) {
-            // Hack to prevent keyboard and insertion handle from showing.
-            cancelLongPress();
-        }
-
-        return super.onTouchEvent(event);
-    }
-
-    @Override
-    public boolean performLongClick() {
-        showContextMenu();
-
-        return true;
-    }
-
-    @Override
-    public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
-        super.onInitializeAccessibilityEvent(event);
-
-        final String mathText = mathParse(getText().toString());
-        // Parse the string into something more "mathematical" sounding.
-        if (!TextUtils.isEmpty(mathText)) {
-            event.getText().clear();
-            event.getText().add(mathText);
-            setContentDescription(mathText);
-        }
-    }
-
-    @Override
-    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
-        super.onInitializeAccessibilityNodeInfo(info);
-
-        info.setText(mathParse(getText().toString()));
-    }
-
-    @Override
-    public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
-        // Do nothing.
-    }
-
-    @Override
-    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
-        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
-
-        mWidthConstraint =
-                MeasureSpec.getSize(widthMeasureSpec) - getPaddingLeft() - getPaddingRight();
-        setVariableFontSize();
-    }
-
-    @Override
-    protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
-        super.onTextChanged(text, start, lengthBefore, lengthAfter);
-
-        if (TextUtils.isEmpty(text)) {
-            setTextSize(TypedValue.COMPLEX_UNIT_PX, mMaximumTextSize);
-            return;
-        }
-
-        setVariableFontSize();
-    }
-
-    private void setVariableFontSize() {
-        if (mWidthConstraint < 0) {
-            // Not measured, bail early.
-            return;
-        }
-
-        final Paint paint = new Paint();
-        final String measureText = getText().toString();
-        int lastFitTextSize = mMinimumTextSize;
-
-        while (lastFitTextSize < mMaximumTextSize) {
-            final int nextSize = lastFitTextSize + mStepTextSize;
-            paint.setTextSize(nextSize);
-            final float measuredTextWidth = paint.measureText(measureText);
-            if (measuredTextWidth > mWidthConstraint) {
-                break;
-            } else {
-                lastFitTextSize = nextSize;
-            }
-        }
-
-        setTextSize(TypedValue.COMPLEX_UNIT_PX, lastFitTextSize);
-    }
-
-    private String mathParse(String plainText) {
-        String parsedText = plainText;
-        if (!TextUtils.isEmpty(parsedText)) {
-            // Initialize replacement table.
-            initializeReplacementTable();
-            for (String operator : sOperators) {
-                if (sReplacementTable.containsKey(operator)) {
-                    parsedText = parsedText.replace(operator, sReplacementTable.get(operator));
-                }
-            }
-        }
-
-        return parsedText;
-    }
-
-    private synchronized void initializeReplacementTable() {
-        if (sReplacementTable == null) {
-            final Resources res = getContext().getResources();
-            final String[] descs = res.getStringArray(R.array.operatorDescs);
-            final String[] ops = res.getStringArray(R.array.operators);
-            final HashMap<String, String> table = new HashMap<String, String>();
-            final int len = ops.length;
-            for (int i = 0; i < len; i++) {
-                table.put(ops[i], descs[i]);
-            }
-
-            sOperators = ops;
-            sReplacementTable = Collections.unmodifiableMap(table);
-        }
-    }
-
-    public boolean onTextContextMenuItem(CharSequence title) {
-        if (TextUtils.equals(title, mMenuItemsStrings[CUT])) {
-            cutContent();
-            return true;
-        } else if (TextUtils.equals(title, mMenuItemsStrings[COPY])) {
-            copyContent();
-            return true;
-        } else if (TextUtils.equals(title, mMenuItemsStrings[PASTE])) {
-            pasteContent();
-            return true;
-        }
-
-        return false;
-    }
-
-    @Override
-    public void onCreateContextMenu(ContextMenu menu) {
-        if (mMenuItemsStrings == null) {
-            final Resources resources = getResources();
-            mMenuItemsStrings = new String[3];
-            mMenuItemsStrings[CUT] = resources.getString(android.R.string.cut);
-            mMenuItemsStrings[COPY] = resources.getString(android.R.string.copy);
-            mMenuItemsStrings[PASTE] = resources.getString(android.R.string.paste);
-        }
-
-        final MenuHandler handler = new MenuHandler();
-        final int len = mMenuItemsStrings.length;
-        for (int i = 0; i < len; i++) {
-            menu.add(Menu.NONE, i, i, mMenuItemsStrings[i]).setOnMenuItemClickListener(handler);
-        }
-
-        if (getText().length() == 0) {
-            menu.getItem(CUT).setVisible(false);
-            menu.getItem(COPY).setVisible(false);
-        }
-
-        final ClipData primaryClip = getPrimaryClip();
-        if (primaryClip == null || primaryClip.getItemCount() == 0
-                || !canPaste(primaryClip.getItemAt(0).coerceToText(getContext()))) {
-            menu.getItem(PASTE).setVisible(false);
-        }
-    }
-
-    private void setPrimaryClip(ClipData clip) {
-        final ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(
-                Context.CLIPBOARD_SERVICE);
-        clipboard.setPrimaryClip(clip);
-    }
-
-    private void copyContent() {
-        final Editable text = getText();
-        final int textLength = text.length();
-        setSelection(0, textLength);
-        setPrimaryClip(ClipData.newPlainText(null, text));
-        setSelection(textLength);
-
-        Toast.makeText(getContext(), R.string.text_copied_toast, Toast.LENGTH_SHORT).show();
-    }
-
-    private void cutContent() {
-        final Editable text = getText();
-        final int textLength = text.length();
-        setSelection(0, textLength);
-        setPrimaryClip(ClipData.newPlainText(null, text));
-        getText().delete(0, textLength);
-        setSelection(0);
-    }
-
-    private ClipData getPrimaryClip() {
-        final ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(
-                Context.CLIPBOARD_SERVICE);
-        return clipboard.getPrimaryClip();
-    }
-
-    private void pasteContent() {
-        final ClipData clip = getPrimaryClip();
-        if (clip != null) {
-            final int len = clip.getItemCount();
-            for (int i = 0; i < len; i++) {
-                final CharSequence paste = clip.getItemAt(i).coerceToText(getContext());
-                if (canPaste(paste)) {
-                    getText().insert(getSelectionEnd(), paste);
-                }
-            }
-        }
-    }
-
-    private boolean canPaste(CharSequence paste) {
-        try {
-            Float.parseFloat(paste.toString());
-            return true;
-        } catch (NumberFormatException e) {
-            Log.e(LOG_TAG, "Error turning string to integer. Ignoring paste.", e);
-            return false;
-        }
-    }
-
-    private class MenuHandler implements MenuItem.OnMenuItemClickListener {
-        @Override
-        public boolean onMenuItemClick(MenuItem item) {
-            return onTextContextMenuItem(item.getTitle());
-        }
-    }
-
-    private class NoTextSelectionMode implements ActionMode.Callback {
+    private final ActionMode.Callback mNoSelectionActionModeCallback = new ActionMode.Callback() {
         @Override
         public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
             return false;
@@ -307,7 +41,6 @@
 
         @Override
         public boolean onCreateActionMode(ActionMode mode, Menu menu) {
-            copyContent();
             // Prevents the selection action mode on double tap.
             return false;
         }
@@ -320,5 +53,118 @@
         public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
             return false;
         }
+    };
+
+    private final float mMaximumTextSize;
+    private final float mMinimumTextSize;
+    private final float mStepTextSize;
+
+    private int mWidthConstraint = -1;
+
+    public CalculatorEditText(Context context) {
+        this(context, null);
+    }
+
+    public CalculatorEditText(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public CalculatorEditText(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+
+        final TypedArray a = context.obtainStyledAttributes(
+                attrs, R.styleable.CalculatorEditText, defStyle, 0);
+        mMaximumTextSize = a.getDimension(
+                R.styleable.CalculatorEditText_maxTextSize, getTextSize());
+        mMinimumTextSize = a.getDimension(
+                R.styleable.CalculatorEditText_minTextSize, getTextSize());
+        mStepTextSize = a.getDimension(R.styleable.CalculatorEditText_stepTextSize,
+                (mMaximumTextSize - mMinimumTextSize) / 3);
+
+        a.recycle();
+
+        setCustomSelectionActionModeCallback(mNoSelectionActionModeCallback);
+        setMovementMethod(ScrollingMovementMethod.getInstance());
+        setTextSize(TypedValue.COMPLEX_UNIT_PX, mMaximumTextSize);
+        setMinHeight(getLineHeight() + getCompoundPaddingBottom() + getCompoundPaddingTop());
+    }
+
+    @Override
+    protected void onSelectionChanged(int selStart, int selEnd) {
+        final int textLength = getText() == null ? 0 : getText().length();
+        if (selStart != textLength || selEnd != textLength) {
+            // Pin the selection to the end of the current text.
+            setSelection(textLength);
+        }
+
+        super.onSelectionChanged(selStart, selEnd);
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        if (event.getActionMasked() == MotionEvent.ACTION_UP) {
+            // Hack to prevent keyboard and insertion handle from showing.
+            cancelLongPress();
+        }
+        return super.onTouchEvent(event);
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+        mWidthConstraint =
+                MeasureSpec.getSize(widthMeasureSpec) - getPaddingLeft() - getPaddingRight();
+        setTextSize(TypedValue.COMPLEX_UNIT_PX, getVariableTextSize(getText().toString()));
+    }
+
+    @Override
+    protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
+        super.onTextChanged(text, start, lengthBefore, lengthAfter);
+
+        setSelection(text.length());
+        setTextSize(TypedValue.COMPLEX_UNIT_PX, getVariableTextSize(text.toString()));
+    }
+
+    public float getVariableTextSize(String text) {
+        if (mWidthConstraint < 0 || mMaximumTextSize <= mMinimumTextSize) {
+            // Not measured, bail early.
+            return getTextSize();
+        }
+
+        final Paint paint = new TextPaint(getPaint());
+        float lastFitTextSize = mMinimumTextSize;
+        while (lastFitTextSize < mMaximumTextSize) {
+            final float nextSize = Math.min(lastFitTextSize + mStepTextSize, mMaximumTextSize);
+            paint.setTextSize(nextSize);
+            if (paint.measureText(text) > mWidthConstraint) {
+                break;
+            } else {
+                lastFitTextSize = nextSize;
+            }
+        }
+
+        return lastFitTextSize;
+    }
+
+    @Override
+    public int getCompoundPaddingTop() {
+        // Measure the top padding from the capital letter height of the text instead of the top,
+        // but don't remove more than the available top padding otherwise clipping may occur.
+        final Rect capBounds = new Rect();
+        getPaint().getTextBounds("H", 0, 1, capBounds);
+
+        final FontMetricsInt fontMetrics = getPaint().getFontMetricsInt();
+        final int paddingOffset = -(fontMetrics.ascent + capBounds.height());
+
+        return super.getCompoundPaddingTop() - Math.min(getPaddingTop(), paddingOffset);
+    }
+
+    @Override
+    public int getCompoundPaddingBottom() {
+        // Measure the bottom padding from the baseline of the text instead of the bottom, but don't
+        // remove more than the available bottom padding otherwise clipping may occur.
+        final FontMetricsInt fontMetrics = getPaint().getFontMetricsInt();
+        return super.getCompoundPaddingBottom() - Math.min(getPaddingBottom(), fontMetrics.descent);
     }
 }