blob: 0b399c4527ede0139c2c30a9f0cc31254a763bbe [file] [log] [blame]
Hans Boehm84614952014-11-25 18:46:17 -08001/*
Hans Boehm24c91ed2016-06-30 18:53:44 -07002 * Copyright (C) 2016 The Android Open Source Project
Hans Boehm84614952014-11-25 18:46:17 -08003 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.calculator2;
18
Chenjie Yu3937b652016-06-01 23:14:26 -070019import android.annotation.TargetApi;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070020import android.content.ClipData;
21import android.content.ClipDescription;
Justin Klaassen44595162015-05-28 17:55:20 -070022import android.content.ClipboardManager;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070023import android.content.Context;
Hans Boehm7f83e362015-06-10 15:41:04 -070024import android.graphics.Rect;
Chenjie Yu3937b652016-06-01 23:14:26 -070025import android.os.Build;
26import android.support.v4.content.ContextCompat;
Justin Klaassenf1b61f42016-04-27 16:00:11 -070027import android.support.v4.os.BuildCompat;
Justin Klaassen44595162015-05-28 17:55:20 -070028import android.text.Layout;
Hans Boehm7f83e362015-06-10 15:41:04 -070029import android.text.Spannable;
Hans Boehm84614952014-11-25 18:46:17 -080030import android.text.SpannableString;
Hans Boehm1176f232015-05-11 16:26:03 -070031import android.text.Spanned;
Justin Klaassen44595162015-05-28 17:55:20 -070032import android.text.TextPaint;
Hans Boehm7f83e362015-06-10 15:41:04 -070033import android.text.style.BackgroundColorSpan;
Hans Boehm84614952014-11-25 18:46:17 -080034import android.text.style.ForegroundColorSpan;
Hans Boehm14344ff2016-06-08 13:01:51 -070035import android.text.style.RelativeSizeSpan;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070036import android.util.AttributeSet;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070037import android.view.ActionMode;
Chenjie Yu3937b652016-06-01 23:14:26 -070038import android.view.ContextMenu;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070039import android.view.GestureDetector;
40import android.view.Menu;
41import android.view.MenuInflater;
42import android.view.MenuItem;
43import android.view.MotionEvent;
44import android.view.View;
Justin Klaassen44595162015-05-28 17:55:20 -070045import android.widget.OverScroller;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070046import android.widget.Toast;
Hans Boehm84614952014-11-25 18:46:17 -080047
Hans Boehm84614952014-11-25 18:46:17 -080048// A text widget that is "infinitely" scrollable to the right,
49// and obtains the text to display via a callback to Logic.
Hans Boehm8f051c32016-10-03 16:53:58 -070050public class CalculatorResult extends AlignedTextView implements MenuItem.OnMenuItemClickListener,
51 Evaluator.EvaluationListener, Evaluator.CharMetricsInfo {
Hans Boehm61568a12015-05-18 18:25:41 -070052 static final int MAX_RIGHT_SCROLL = 10000000;
Hans Boehm08e8f322015-04-21 13:18:38 -070053 static final int INVALID = MAX_RIGHT_SCROLL + 10000;
Hans Boehm84614952014-11-25 18:46:17 -080054 // A larger value is unlikely to avoid running out of space
55 final OverScroller mScroller;
56 final GestureDetector mGestureDetector;
Hans Boehm8f051c32016-10-03 16:53:58 -070057 private long mIndex; // Index of expression we are displaying.
Hans Boehm84614952014-11-25 18:46:17 -080058 private Evaluator mEvaluator;
59 private boolean mScrollable = false;
60 // A scrollable result is currently displayed.
Hans Boehm760a9dc2015-04-20 10:27:12 -070061 private boolean mValid = false;
Hans Boehmc01cd7f2015-05-12 18:32:19 -070062 // The result holds something valid; either a a number or an error
63 // message.
Hans Boehm5e802f32015-06-22 17:18:52 -070064 // A suffix of "Pos" denotes a pixel offset. Zero represents a scroll position
65 // in which the decimal point is just barely visible on the right of the display.
Hans Boehmc01cd7f2015-05-12 18:32:19 -070066 private int mCurrentPos;// Position of right of display relative to decimal point, in pixels.
67 // Large positive values mean the decimal point is scrolled off the
68 // left of the display. Zero means decimal point is barely displayed
69 // on the right.
Hans Boehm61568a12015-05-18 18:25:41 -070070 private int mLastPos; // Position already reflected in display. Pixels.
Hans Boehm65a99a42016-02-03 18:16:07 -080071 private int mMinPos; // Minimum position to avoid unnecessary blanks on the left. Pixels.
Hans Boehm61568a12015-05-18 18:25:41 -070072 private int mMaxPos; // Maximum position before we start displaying the infinite
73 // sequence of trailing zeroes on the right. Pixels.
Hans Boehm65a99a42016-02-03 18:16:07 -080074 private int mWholeLen; // Length of the whole part of current result.
Hans Boehm5e802f32015-06-22 17:18:52 -070075 // In the following, we use a suffix of Offset to denote a character position in a numeric
76 // string relative to the decimal point. Positive is to the right and negative is to
77 // the left. 1 = tenths position, -1 = units. Integer.MAX_VALUE is sometimes used
78 // for the offset of the last digit in an a nonterminating decimal expansion.
79 // We use the suffix "Index" to denote a zero-based index into a string representing a
80 // result.
Hans Boehm5e802f32015-06-22 17:18:52 -070081 private int mMaxCharOffset; // Character offset from decimal point of rightmost digit
Hans Boehm24c91ed2016-06-30 18:53:44 -070082 // that should be displayed, plus the length of any exponent
83 // needed to display that digit.
84 // Limited to MAX_RIGHT_SCROLL. Often the same as:
Hans Boehm5e802f32015-06-22 17:18:52 -070085 private int mLsdOffset; // Position of least-significant digit in result
86 private int mLastDisplayedOffset; // Offset of last digit actually displayed after adding
Hans Boehmf6dae112015-06-18 17:57:50 -070087 // exponent.
Hans Boehm24c91ed2016-06-30 18:53:44 -070088 private boolean mWholePartFits; // Scientific notation not needed for initial display.
89 private float mNoExponentCredit;
90 // Fraction of digit width saved by avoiding scientific notation.
91 // Only accessed from UI thread.
92 private boolean mAppendExponent;
93 // The result fits entirely in the display, even with an exponent,
94 // but not with grouping separators. Since the result is not
95 // scrollable, and we do not add the exponent to max. scroll position,
96 // append an exponent insteadd of replacing trailing digits.
Justin Klaassen44595162015-05-28 17:55:20 -070097 private final Object mWidthLock = new Object();
Hans Boehm24c91ed2016-06-30 18:53:44 -070098 // Protects the next five fields. These fields are only
99 // Updated by the UI thread, and read accesses by the UI thread
100 // sometimes do not acquire the lock.
Hans Boehmd4959e82016-11-15 18:01:28 -0800101 private int mWidthConstraint = 0;
Hans Boehma0e45f32015-05-30 13:20:35 -0700102 // Our total width in pixels minus space for ellipsis.
Hans Boehmd4959e82016-11-15 18:01:28 -0800103 // 0 ==> uninitialized.
Justin Klaassen44595162015-05-28 17:55:20 -0700104 private float mCharWidth = 1;
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700105 // Maximum character width. For now we pretend that all characters
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700106 // have this width.
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700107 // TODO: We're not really using a fixed width font. But it appears
108 // to be close enough for the characters we use that the difference
109 // is not noticeable.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700110 private float mGroupingSeparatorWidthRatio;
111 // Fraction of digit width occupied by a digit separator.
112 private float mDecimalCredit;
113 // Fraction of digit width saved by replacing digit with decimal point.
114 private float mNoEllipsisCredit;
115 // Fraction of digit width saved by both replacing ellipsis with digit
116 // and avoiding scientific notation.
Annie Chinbc001882016-11-09 19:41:21 -0800117 private boolean mShouldRequireResult = true;
Hans Boehmd4959e82016-11-15 18:01:28 -0800118 private Evaluator.EvaluationListener mEvaluationListener = this;
119 // Listener to use if/when evaluation is requested.
Hans Boehm50ed3202015-06-09 14:35:49 -0700120 public static final int MAX_LEADING_ZEROES = 6;
Hans Boehma0e45f32015-05-30 13:20:35 -0700121 // Maximum number of leading zeroes after decimal point before we
122 // switch to scientific notation with negative exponent.
Hans Boehm50ed3202015-06-09 14:35:49 -0700123 public static final int MAX_TRAILING_ZEROES = 6;
Hans Boehma0e45f32015-05-30 13:20:35 -0700124 // Maximum number of trailing zeroes before the decimal point before
125 // we switch to scientific notation with positive exponent.
126 private static final int SCI_NOTATION_EXTRA = 1;
127 // Extra digits for standard scientific notation. In this case we
Hans Boehm80018c82015-08-02 16:59:07 -0700128 // have a decimal point and no ellipsis.
129 // We assume that we do not drop digits to make room for the decimal
130 // point in ordinary scientific notation. Thus >= 1.
Hans Boehm65a99a42016-02-03 18:16:07 -0800131 private static final int MAX_COPY_EXTRA = 100;
132 // The number of extra digits we are willing to compute to copy
133 // a result as an exact number.
134 private static final int MAX_RECOMPUTE_DIGITS = 2000;
135 // The maximum number of digits we're willing to recompute in the UI
136 // thread. We only do this for known rational results, where we
137 // can bound the computation cost.
Chenjie Yu3937b652016-06-01 23:14:26 -0700138 private final ForegroundColorSpan mExponentColorSpan;
139 private final BackgroundColorSpan mHighlightSpan;
Hans Boehm65a99a42016-02-03 18:16:07 -0800140
Hans Boehm1176f232015-05-11 16:26:03 -0700141 private ActionMode mActionMode;
Chenjie Yu3937b652016-06-01 23:14:26 -0700142 private ActionMode.Callback mCopyActionModeCallback;
143 private ContextMenu mContextMenu;
Hans Boehm84614952014-11-25 18:46:17 -0800144
Annie Chin37c33b62016-11-22 14:46:28 -0800145 // The user requested that the result currently being evaluated should be stored to "memory".
146 private boolean mStoreToMemoryRequested = false;
147
Hans Boehm84614952014-11-25 18:46:17 -0800148 public CalculatorResult(Context context, AttributeSet attrs) {
149 super(context, attrs);
150 mScroller = new OverScroller(context);
Chenjie Yu3937b652016-06-01 23:14:26 -0700151 mHighlightSpan = new BackgroundColorSpan(getHighlightColor());
152 mExponentColorSpan = new ForegroundColorSpan(
153 ContextCompat.getColor(context, R.color.display_result_exponent_text_color));
Hans Boehm84614952014-11-25 18:46:17 -0800154 mGestureDetector = new GestureDetector(context,
155 new GestureDetector.SimpleOnGestureListener() {
156 @Override
Justin Klaassend48b7562015-04-16 16:51:38 -0700157 public boolean onDown(MotionEvent e) {
158 return true;
159 }
160 @Override
Hans Boehm84614952014-11-25 18:46:17 -0800161 public boolean onFling(MotionEvent e1, MotionEvent e2,
162 float velocityX, float velocityY) {
163 if (!mScroller.isFinished()) {
164 mCurrentPos = mScroller.getFinalX();
165 }
166 mScroller.forceFinished(true);
Chenjie Yu3937b652016-06-01 23:14:26 -0700167 stopActionModeOrContextMenu();
Hans Boehmfbcef702015-04-27 18:07:47 -0700168 CalculatorResult.this.cancelLongPress();
169 // Ignore scrolls of error string, etc.
170 if (!mScrollable) return true;
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700171 mScroller.fling(mCurrentPos, 0, - (int) velocityX, 0 /* horizontal only */,
Hans Boehm61568a12015-05-18 18:25:41 -0700172 mMinPos, mMaxPos, 0, 0);
Justin Klaassen44595162015-05-28 17:55:20 -0700173 postInvalidateOnAnimation();
Hans Boehm84614952014-11-25 18:46:17 -0800174 return true;
175 }
176 @Override
177 public boolean onScroll(MotionEvent e1, MotionEvent e2,
178 float distanceX, float distanceY) {
Hans Boehm61568a12015-05-18 18:25:41 -0700179 int distance = (int)distanceX;
Hans Boehm84614952014-11-25 18:46:17 -0800180 if (!mScroller.isFinished()) {
181 mCurrentPos = mScroller.getFinalX();
182 }
183 mScroller.forceFinished(true);
Chenjie Yu3937b652016-06-01 23:14:26 -0700184 stopActionModeOrContextMenu();
Hans Boehm84614952014-11-25 18:46:17 -0800185 CalculatorResult.this.cancelLongPress();
186 if (!mScrollable) return true;
Hans Boehm61568a12015-05-18 18:25:41 -0700187 if (mCurrentPos + distance < mMinPos) {
188 distance = mMinPos - mCurrentPos;
189 } else if (mCurrentPos + distance > mMaxPos) {
190 distance = mMaxPos - mCurrentPos;
191 }
Hans Boehm84614952014-11-25 18:46:17 -0800192 int duration = (int)(e2.getEventTime() - e1.getEventTime());
193 if (duration < 1 || duration > 100) duration = 10;
Hans Boehm61568a12015-05-18 18:25:41 -0700194 mScroller.startScroll(mCurrentPos, 0, distance, 0, (int)duration);
Justin Klaassen44595162015-05-28 17:55:20 -0700195 postInvalidateOnAnimation();
Hans Boehm84614952014-11-25 18:46:17 -0800196 return true;
197 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700198 @Override
199 public void onLongPress(MotionEvent e) {
Hans Boehm1176f232015-05-11 16:26:03 -0700200 if (mValid) {
Justin Klaassen3a05c7e2016-03-04 12:40:02 -0800201 performLongClick();
Hans Boehm1176f232015-05-11 16:26:03 -0700202 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700203 }
Hans Boehm84614952014-11-25 18:46:17 -0800204 });
Justin Klaassen3a05c7e2016-03-04 12:40:02 -0800205 setOnTouchListener(new View.OnTouchListener() {
206 @Override
207 public boolean onTouch(View v, MotionEvent event) {
208 return mGestureDetector.onTouchEvent(event);
209 }
210 });
Hans Boehm14344ff2016-06-08 13:01:51 -0700211
Chenjie Yu3937b652016-06-01 23:14:26 -0700212 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
213 setupActionMode();
214 } else {
215 setupContextMenu();
216 }
Hans Boehm14344ff2016-06-08 13:01:51 -0700217
Hans Boehm84614952014-11-25 18:46:17 -0800218 setCursorVisible(false);
Christine Franksafe28bb2016-07-29 17:24:52 -0700219 setLongClickable(false);
Christine Franks6f6c24a2016-09-08 18:21:47 -0700220 setContentDescription(context.getString(R.string.desc_result));
Hans Boehm84614952014-11-25 18:46:17 -0800221 }
222
Hans Boehm8f051c32016-10-03 16:53:58 -0700223 void setEvaluator(Evaluator evaluator, long index) {
Hans Boehm84614952014-11-25 18:46:17 -0800224 mEvaluator = evaluator;
Hans Boehm8f051c32016-10-03 16:53:58 -0700225 mIndex = index;
Annie Chin7c586042016-11-18 15:57:37 -0800226 requestLayout();
Hans Boehm84614952014-11-25 18:46:17 -0800227 }
228
Hans Boehmcd72f7e2016-06-01 16:21:25 -0700229 // Compute maximum digit width the hard way.
230 private static float getMaxDigitWidth(TextPaint paint) {
231 // Compute the maximum advance width for each digit, thus accounting for between-character
232 // spaces. If we ever support other kinds of digits, we may have to avoid kerning effects
233 // that could reduce the advance width within this particular string.
234 final String allDigits = "0123456789";
235 final float[] widths = new float[allDigits.length()];
236 paint.getTextWidths(allDigits, widths);
237 float maxWidth = 0;
238 for (float x : widths) {
239 maxWidth = Math.max(x, maxWidth);
240 }
241 return maxWidth;
242 }
243
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700244 @Override
245 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Justin Klaassend06f51d2016-08-03 00:41:31 -0700246 if (!isLaidOut()) {
247 // Set a minimum height so scaled error messages won't affect our layout.
248 setMinimumHeight(getLineHeight() + getCompoundPaddingBottom()
249 + getCompoundPaddingTop());
250 }
251
Justin Klaassen44595162015-05-28 17:55:20 -0700252 final TextPaint paint = getPaint();
Hans Boehm80018c82015-08-02 16:59:07 -0700253 final Context context = getContext();
Hans Boehmcd72f7e2016-06-01 16:21:25 -0700254 final float newCharWidth = getMaxDigitWidth(paint);
Hans Boehm80018c82015-08-02 16:59:07 -0700255 // Digits are presumed to have no more than newCharWidth.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700256 // There are two instances when we know that the result is otherwise narrower than
257 // expected:
258 // 1. For standard scientific notation (our type 1), we know that we have a norrow decimal
259 // point and no (usually wide) ellipsis symbol. We allow one extra digit
260 // (SCI_NOTATION_EXTRA) to compensate, and consider that in determining available width.
261 // 2. If we are using digit grouping separators and a decimal point, we give ourselves
262 // a fractional extra space for those separators, the value of which depends on whether
263 // there is also an ellipsis.
264 //
265 // Maximum extra space we need in various cases:
266 // Type 1 scientific notation, assuming ellipsis, minus sign and E are wider than a digit:
267 // Two minus signs + "E" + "." - 3 digits.
268 // Type 2 scientific notation:
269 // Ellipsis + "E" + "-" - 3 digits.
270 // In the absence of scientific notation, we may need a little less space.
271 // We give ourselves a bit of extra credit towards comma insertion and give
272 // ourselves more if we have either
273 // No ellipsis, or
274 // A decimal separator.
275
276 // Calculate extra space we need to reserve, in addition to character count.
Hans Boehm80018c82015-08-02 16:59:07 -0700277 final float decimalSeparatorWidth = Layout.getDesiredWidth(
278 context.getString(R.string.dec_point), paint);
Hans Boehm24c91ed2016-06-30 18:53:44 -0700279 final float minusWidth = Layout.getDesiredWidth(context.getString(R.string.op_sub), paint);
280 final float minusExtraWidth = Math.max(minusWidth - newCharWidth, 0.0f);
281 final float ellipsisWidth = Layout.getDesiredWidth(KeyMaps.ELLIPSIS, paint);
282 final float ellipsisExtraWidth = Math.max(ellipsisWidth - newCharWidth, 0.0f);
283 final float expWidth = Layout.getDesiredWidth(KeyMaps.translateResult("e"), paint);
284 final float expExtraWidth = Math.max(expWidth - newCharWidth, 0.0f);
285 final float type1Extra = 2 * minusExtraWidth + expExtraWidth + decimalSeparatorWidth;
286 final float type2Extra = ellipsisExtraWidth + expExtraWidth + minusExtraWidth;
287 final float extraWidth = Math.max(type1Extra, type2Extra);
288 final int intExtraWidth = (int) Math.ceil(extraWidth) + 1 /* to cover rounding sins */;
Hans Boehm80018c82015-08-02 16:59:07 -0700289 final int newWidthConstraint = MeasureSpec.getSize(widthMeasureSpec)
Hans Boehm24c91ed2016-06-30 18:53:44 -0700290 - (getPaddingLeft() + getPaddingRight()) - intExtraWidth;
291
292 // Calculate other width constants we need to handle grouping separators.
293 final float groupingSeparatorW =
294 Layout.getDesiredWidth(KeyMaps.translateResult(","), paint);
295 // Credits in the absence of any scientific notation:
296 float noExponentCredit = extraWidth - Math.max(ellipsisExtraWidth, minusExtraWidth);
297 final float noEllipsisCredit = extraWidth - minusExtraWidth; // includes noExponentCredit.
298 final float decimalCredit = Math.max(newCharWidth - decimalSeparatorWidth, 0.0f);
299
300 mNoExponentCredit = noExponentCredit / newCharWidth;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700301 synchronized(mWidthLock) {
Hans Boehm013969e2015-04-13 20:29:47 -0700302 mWidthConstraint = newWidthConstraint;
303 mCharWidth = newCharWidth;
Hans Boehm24c91ed2016-06-30 18:53:44 -0700304 mNoEllipsisCredit = noEllipsisCredit / newCharWidth;
305 mDecimalCredit = decimalCredit / newCharWidth;
306 mGroupingSeparatorWidthRatio = groupingSeparatorW / newCharWidth;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700307 }
Hans Boehm14344ff2016-06-08 13:01:51 -0700308
309 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700310 }
311
Annie Chin06fd3cf2016-11-07 16:04:33 -0800312 @Override
313 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
314 super.onLayout(changed, left, top, right, bottom);
315
Annie Chinbc001882016-11-09 19:41:21 -0800316 if (mEvaluator != null && mShouldRequireResult) {
Annie Chin06fd3cf2016-11-07 16:04:33 -0800317 final CalculatorExpr expr = mEvaluator.getExpr(mIndex);
318 if (expr != null && expr.hasInterestingOps()) {
Hans Boehmd4959e82016-11-15 18:01:28 -0800319 mEvaluator.requireResult(mIndex, mEvaluationListener, this);
Annie Chin06fd3cf2016-11-07 16:04:33 -0800320 }
321 }
322 }
323
Hans Boehmd4959e82016-11-15 18:01:28 -0800324 public void setShouldRequireResult(boolean should, Evaluator.EvaluationListener listener) {
325 mEvaluationListener = listener;
Annie Chinbc001882016-11-09 19:41:21 -0800326 mShouldRequireResult = should;
327 }
328
Hans Boehm8f051c32016-10-03 16:53:58 -0700329 // From Evaluator.CharMetricsInfo.
330 @Override
Hans Boehm24c91ed2016-06-30 18:53:44 -0700331 public float separatorChars(String s, int len) {
332 int start = 0;
333 while (start < len && !Character.isDigit(s.charAt(start))) {
334 ++start;
335 }
336 // We assume the rest consists of digits, and for consistency with the rest
337 // of the code, we assume all digits have width mCharWidth.
338 final int nDigits = len - start;
339 // We currently insert a digit separator every three digits.
340 final int nSeparators = (nDigits - 1) / 3;
341 synchronized(mWidthLock) {
342 // Always return an upper bound, even in the presence of rounding errors.
343 return nSeparators * mGroupingSeparatorWidthRatio;
344 }
345 }
346
Hans Boehm8f051c32016-10-03 16:53:58 -0700347 // From Evaluator.CharMetricsInfo.
348 @Override
Hans Boehm24c91ed2016-06-30 18:53:44 -0700349 public float getNoEllipsisCredit() {
350 synchronized(mWidthLock) {
351 return mNoEllipsisCredit;
352 }
353 }
354
Hans Boehm8f051c32016-10-03 16:53:58 -0700355 // From Evaluator.CharMetricsInfo.
356 @Override
Hans Boehm24c91ed2016-06-30 18:53:44 -0700357 public float getDecimalCredit() {
358 synchronized(mWidthLock) {
359 return mDecimalCredit;
360 }
361 }
362
Hans Boehma0e45f32015-05-30 13:20:35 -0700363 // Return the length of the exponent representation for the given exponent, in
364 // characters.
365 private final int expLen(int exp) {
366 if (exp == 0) return 0;
Hans Boehm5e802f32015-06-22 17:18:52 -0700367 final int abs_exp_digits = (int) Math.ceil(Math.log10(Math.abs((double)exp))
368 + 0.0000000001d /* Round whole numbers to next integer */);
369 return abs_exp_digits + (exp >= 0 ? 1 : 2);
Hans Boehm61568a12015-05-18 18:25:41 -0700370 }
371
Hans Boehma0e45f32015-05-30 13:20:35 -0700372 /**
373 * Initiate display of a new result.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700374 * Only called from UI thread.
Hans Boehma0e45f32015-05-30 13:20:35 -0700375 * The parameters specify various properties of the result.
Hans Boehm8f051c32016-10-03 16:53:58 -0700376 * @param index Index of expression that was just evaluated. Currently ignored, since we only
377 * expect notification for the expression result being displayed.
Hans Boehma0e45f32015-05-30 13:20:35 -0700378 * @param initPrec Initial display precision computed by evaluator. (1 = tenths digit)
379 * @param msd Position of most significant digit. Offset from left of string.
380 Evaluator.INVALID_MSD if unknown.
381 * @param leastDigPos Position of least significant digit (1 = tenths digit)
382 * or Integer.MAX_VALUE.
383 * @param truncatedWholePart Result up to but not including decimal point.
384 Currently we only use the length.
385 */
Hans Boehm8f051c32016-10-03 16:53:58 -0700386 @Override
387 public void onEvaluate(long index, int initPrec, int msd, int leastDigPos,
388 String truncatedWholePart) {
Hans Boehma0e45f32015-05-30 13:20:35 -0700389 initPositions(initPrec, msd, leastDigPos, truncatedWholePart);
Annie Chin37c33b62016-11-22 14:46:28 -0800390
391 if (mStoreToMemoryRequested) {
392 mEvaluator.copyToMemory(index);
393 mStoreToMemoryRequested = false;
394 }
Hans Boehm84614952014-11-25 18:46:17 -0800395 redisplay();
396 }
397
Hans Boehma0e45f32015-05-30 13:20:35 -0700398 /**
Annie Chin37c33b62016-11-22 14:46:28 -0800399 * Store the result for this index if it is available.
400 * If it is unavailable, set mStoreToMemoryRequested to indicate that we should store
401 * when evaluation is complete.
402 */
403 public void onMemoryStore() {
404 if (mEvaluator.hasResult(mIndex)) {
405 mEvaluator.copyToMemory(mIndex);
406 } else {
407 mStoreToMemoryRequested = true;
408 mEvaluator.requireResult(mIndex, this /* listener */, this /* CharMetricsInfo */);
409 }
410 }
411
412 /**
Christine Franks1d99be12016-11-14 14:00:36 -0800413 * Add the result to the value currently in memory.
414 */
415 public void onMemoryAdd() {
416 mEvaluator.addToMemory(Evaluator.MAIN_INDEX);
417 }
418
419 /**
420 * Subtract the result from the value currently in memory.
421 */
422 public void onMemorySubtract() {
423 mEvaluator.subtractFromMemory(Evaluator.MAIN_INDEX);
424 }
425
426 /**
Hans Boehm5e802f32015-06-22 17:18:52 -0700427 * Set up scroll bounds (mMinPos, mMaxPos, etc.) and determine whether the result is
428 * scrollable, based on the supplied information about the result.
Hans Boehma0e45f32015-05-30 13:20:35 -0700429 * This is unfortunately complicated because we need to predict whether trailing digits
430 * will eventually be replaced by an exponent.
431 * Just appending the exponent during formatting would be simpler, but would produce
432 * jumpier results during transitions.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700433 * Only called from UI thread.
Hans Boehma0e45f32015-05-30 13:20:35 -0700434 */
Hans Boehm5e802f32015-06-22 17:18:52 -0700435 private void initPositions(int initPrecOffset, int msdIndex, int lsdOffset,
436 String truncatedWholePart) {
Hans Boehma0e45f32015-05-30 13:20:35 -0700437 int maxChars = getMaxChars();
Hans Boehm24c91ed2016-06-30 18:53:44 -0700438 mWholeLen = truncatedWholePart.length();
439 // Allow a tiny amount of slop for associativity/rounding differences in length
440 // calculation. If getPreferredPrec() decided it should fit, we want to make it fit, too.
441 // We reserved one extra pixel, so the extra length is OK.
442 final int nSeparatorChars = (int) Math.ceil(
443 separatorChars(truncatedWholePart, truncatedWholePart.length())
444 - getNoEllipsisCredit() - 0.0001f);
445 mWholePartFits = mWholeLen + nSeparatorChars <= maxChars;
Hans Boehma0e45f32015-05-30 13:20:35 -0700446 mLastPos = INVALID;
Hans Boehm5e802f32015-06-22 17:18:52 -0700447 mLsdOffset = lsdOffset;
Hans Boehm24c91ed2016-06-30 18:53:44 -0700448 mAppendExponent = false;
Hans Boehma0e45f32015-05-30 13:20:35 -0700449 // Prevent scrolling past initial position, which is calculated to show leading digits.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700450 mCurrentPos = mMinPos = (int) Math.round(initPrecOffset * mCharWidth);
Hans Boehm5e802f32015-06-22 17:18:52 -0700451 if (msdIndex == Evaluator.INVALID_MSD) {
Hans Boehma0e45f32015-05-30 13:20:35 -0700452 // Possible zero value
Hans Boehm5e802f32015-06-22 17:18:52 -0700453 if (lsdOffset == Integer.MIN_VALUE) {
Hans Boehma0e45f32015-05-30 13:20:35 -0700454 // Definite zero value.
455 mMaxPos = mMinPos;
Hans Boehm24c91ed2016-06-30 18:53:44 -0700456 mMaxCharOffset = (int) Math.round(mMaxPos/mCharWidth);
Hans Boehma0e45f32015-05-30 13:20:35 -0700457 mScrollable = false;
458 } else {
459 // May be very small nonzero value. Allow user to find out.
Hans Boehm5e802f32015-06-22 17:18:52 -0700460 mMaxPos = mMaxCharOffset = MAX_RIGHT_SCROLL;
Hans Boehm24c91ed2016-06-30 18:53:44 -0700461 mMinPos -= mCharWidth; // Allow for future minus sign.
Hans Boehma0e45f32015-05-30 13:20:35 -0700462 mScrollable = true;
463 }
464 return;
465 }
Hans Boehma0e45f32015-05-30 13:20:35 -0700466 int negative = truncatedWholePart.charAt(0) == '-' ? 1 : 0;
Hans Boehm65a99a42016-02-03 18:16:07 -0800467 if (msdIndex > mWholeLen && msdIndex <= mWholeLen + 3) {
Hans Boehm5e802f32015-06-22 17:18:52 -0700468 // Avoid tiny negative exponent; pretend msdIndex is just to the right of decimal point.
Hans Boehm65a99a42016-02-03 18:16:07 -0800469 msdIndex = mWholeLen - 1;
Hans Boehma0e45f32015-05-30 13:20:35 -0700470 }
Hans Boehm24c91ed2016-06-30 18:53:44 -0700471 // Set to position of leftmost significant digit relative to dec. point. Usually negative.
Hans Boehm65a99a42016-02-03 18:16:07 -0800472 int minCharOffset = msdIndex - mWholeLen;
Hans Boehm5e802f32015-06-22 17:18:52 -0700473 if (minCharOffset > -1 && minCharOffset < MAX_LEADING_ZEROES + 2) {
Hans Boehma0e45f32015-05-30 13:20:35 -0700474 // Small number of leading zeroes, avoid scientific notation.
Hans Boehm5e802f32015-06-22 17:18:52 -0700475 minCharOffset = -1;
Hans Boehma0e45f32015-05-30 13:20:35 -0700476 }
Hans Boehm5e802f32015-06-22 17:18:52 -0700477 if (lsdOffset < MAX_RIGHT_SCROLL) {
478 mMaxCharOffset = lsdOffset;
479 if (mMaxCharOffset < -1 && mMaxCharOffset > -(MAX_TRAILING_ZEROES + 2)) {
480 mMaxCharOffset = -1;
Hans Boehma0e45f32015-05-30 13:20:35 -0700481 }
Hans Boehm5e802f32015-06-22 17:18:52 -0700482 // lsdOffset is positive or negative, never 0.
483 int currentExpLen = 0; // Length of required standard scientific notation exponent.
484 if (mMaxCharOffset < -1) {
485 currentExpLen = expLen(-minCharOffset - 1);
486 } else if (minCharOffset > -1 || mMaxCharOffset >= maxChars) {
Hans Boehm24c91ed2016-06-30 18:53:44 -0700487 // Number is either entirely to the right of decimal point, or decimal point is
488 // not visible when scrolled to the right.
Hans Boehm5e802f32015-06-22 17:18:52 -0700489 currentExpLen = expLen(-minCharOffset);
Hans Boehma0e45f32015-05-30 13:20:35 -0700490 }
Hans Boehm24c91ed2016-06-30 18:53:44 -0700491 // Exponent length does not included added decimal point. But whenever we add a
492 // decimal point, we allow an extra character (SCI_NOTATION_EXTRA).
493 final int separatorLength = mWholePartFits && minCharOffset < -3 ? nSeparatorChars : 0;
494 mScrollable = (mMaxCharOffset + currentExpLen + separatorLength - minCharOffset
495 + negative >= maxChars);
496 // Now adjust mMaxCharOffset for any required exponent.
Hans Boehm5e802f32015-06-22 17:18:52 -0700497 int newMaxCharOffset;
498 if (currentExpLen > 0) {
499 if (mScrollable) {
500 // We'll use exponent corresponding to leastDigPos when scrolled to right.
501 newMaxCharOffset = mMaxCharOffset + expLen(-lsdOffset);
502 } else {
503 newMaxCharOffset = mMaxCharOffset + currentExpLen;
504 }
505 if (mMaxCharOffset <= -1 && newMaxCharOffset > -1) {
506 // Very unlikely; just drop exponent.
507 mMaxCharOffset = -1;
508 } else {
Hans Boehm24c91ed2016-06-30 18:53:44 -0700509 mMaxCharOffset = Math.min(newMaxCharOffset, MAX_RIGHT_SCROLL);
Hans Boehma0e45f32015-05-30 13:20:35 -0700510 }
Hans Boehm24c91ed2016-06-30 18:53:44 -0700511 mMaxPos = Math.min((int) Math.round(mMaxCharOffset * mCharWidth),
512 MAX_RIGHT_SCROLL);
513 } else if (!mWholePartFits && !mScrollable) {
514 // Corner case in which entire number fits, but not with grouping separators. We
515 // will use an exponent in un-scrolled position, which may hide digits. Scrolling
516 // by one character will remove the exponent and reveal the last digits. Note
517 // that in the forced scientific notation case, the exponent length is not
518 // factored into mMaxCharOffset, since we do not want such an increase to impact
519 // scrolling behavior. In the unscrollable case, we thus have to append the
520 // exponent at the end using the forcePrecision argument to formatResult, in order
521 // to ensure that we get the entire result.
522 mScrollable = (mMaxCharOffset + expLen(-minCharOffset - 1) - minCharOffset
523 + negative >= maxChars);
524 if (mScrollable) {
525 mMaxPos = (int) Math.ceil(mMinPos + mCharWidth);
526 // Single character scroll will remove exponent and show remaining piece.
527 } else {
528 mMaxPos = mMinPos;
529 mAppendExponent = true;
530 }
531 } else {
532 mMaxPos = Math.min((int) Math.round(mMaxCharOffset * mCharWidth),
533 MAX_RIGHT_SCROLL);
Hans Boehma0e45f32015-05-30 13:20:35 -0700534 }
Hans Boehma0e45f32015-05-30 13:20:35 -0700535 if (!mScrollable) {
536 // Position the number consistently with our assumptions to make sure it
537 // actually fits.
538 mCurrentPos = mMaxPos;
539 }
540 } else {
Hans Boehm5e802f32015-06-22 17:18:52 -0700541 mMaxPos = mMaxCharOffset = MAX_RIGHT_SCROLL;
Hans Boehma0e45f32015-05-30 13:20:35 -0700542 mScrollable = true;
543 }
544 }
545
Hans Boehm24c91ed2016-06-30 18:53:44 -0700546 /**
547 * Display error message indicated by resourceId.
548 * UI thread only.
549 */
Hans Boehm8f051c32016-10-03 16:53:58 -0700550 @Override
551 public void onError(long index, int resourceId) {
Annie Chin37c33b62016-11-22 14:46:28 -0800552 mStoreToMemoryRequested = false;
Hans Boehm760a9dc2015-04-20 10:27:12 -0700553 mValid = true;
Christine Franksafe28bb2016-07-29 17:24:52 -0700554 setLongClickable(false);
Hans Boehm84614952014-11-25 18:46:17 -0800555 mScrollable = false;
Hans Boehm14344ff2016-06-08 13:01:51 -0700556 final String msg = getContext().getString(resourceId);
Hans Boehm14344ff2016-06-08 13:01:51 -0700557 final float measuredWidth = Layout.getDesiredWidth(msg, getPaint());
Hans Boehm24c91ed2016-06-30 18:53:44 -0700558 if (measuredWidth > mWidthConstraint) {
Hans Boehm14344ff2016-06-08 13:01:51 -0700559 // Multiply by .99 to avoid rounding effects.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700560 final float scaleFactor = 0.99f * mWidthConstraint / measuredWidth;
Hans Boehm14344ff2016-06-08 13:01:51 -0700561 final RelativeSizeSpan smallTextSpan = new RelativeSizeSpan(scaleFactor);
562 final SpannableString scaledMsg = new SpannableString(msg);
563 scaledMsg.setSpan(smallTextSpan, 0, msg.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
564 setText(scaledMsg);
565 } else {
566 setText(msg);
567 }
Hans Boehm84614952014-11-25 18:46:17 -0800568 }
569
Hans Boehm013969e2015-04-13 20:29:47 -0700570 private final int MAX_COPY_SIZE = 1000000;
571
Hans Boehma0e45f32015-05-30 13:20:35 -0700572 /*
573 * Return the most significant digit position in the given string or Evaluator.INVALID_MSD.
Hans Boehm3666e632015-07-27 18:33:12 -0700574 * Unlike Evaluator.getMsdIndexOf, we treat a final 1 as significant.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700575 * Pure function; callable from anywhere.
Hans Boehma0e45f32015-05-30 13:20:35 -0700576 */
Hans Boehm3666e632015-07-27 18:33:12 -0700577 public static int getNaiveMsdIndexOf(String s) {
Hans Boehm65a99a42016-02-03 18:16:07 -0800578 final int len = s.length();
Hans Boehma0e45f32015-05-30 13:20:35 -0700579 for (int i = 0; i < len; ++i) {
580 char c = s.charAt(i);
581 if (c != '-' && c != '.' && c != '0') {
582 return i;
583 }
584 }
585 return Evaluator.INVALID_MSD;
586 }
587
Hans Boehm24c91ed2016-06-30 18:53:44 -0700588 /**
589 * Format a result returned by Evaluator.getString() into a single line containing ellipses
590 * (if appropriate) and an exponent (if appropriate).
591 * We add two distinct kinds of exponents:
592 * (1) If the final result contains the leading digit we use standard scientific notation.
593 * (2) If not, we add an exponent corresponding to an interpretation of the final result as
594 * an integer.
595 * We add an ellipsis on the left if the result was truncated.
596 * We add ellipses and exponents in a way that leaves most digits in the position they
597 * would have been in had we not done so. This minimizes jumps as a result of scrolling.
598 * Result is NOT internationalized, uses "E" for exponent.
599 * Called only from UI thread; We sometimes omit locking for fields.
600 * @param precOffset The value that was passed to getString. Identifies the significance of
601 the rightmost digit. A value of 1 means the rightmost digits corresponds to tenths.
602 * @param maxDigs The maximum number of characters in the result
603 * @param truncated The in parameter was already truncated, beyond possibly removing the
604 minus sign.
605 * @param negative The in parameter represents a negative result. (Minus sign may be removed
606 without setting truncated.)
607 * @param lastDisplayedOffset If not null, we set lastDisplayedOffset[0] to the offset of
608 the last digit actually appearing in the display.
609 * @param forcePrecision If true, we make sure that the last displayed digit corresponds to
610 precOffset, and allow maxDigs to be exceeded in adding the exponent and commas.
611 * @param forceSciNotation Force scientific notation. May be set because we don't have
612 space for grouping separators, but whole number otherwise fits.
613 * @param insertCommas Insert commas (literally, not internationalized) as digit separators.
614 We only ever do this for the integral part of a number, and only when no
615 exponent is displayed in the initial position. The combination of which means
616 that we only do it when no exponent is displayed.
617 We insert commas in a way that does consider the width of the actual localized digit
618 separator. Commas count towards maxDigs as the appropriate fraction of a digit.
619 */
620 private String formatResult(String in, int precOffset, int maxDigs, boolean truncated,
621 boolean negative, int lastDisplayedOffset[], boolean forcePrecision,
622 boolean forceSciNotation, boolean insertCommas) {
Hans Boehm5e802f32015-06-22 17:18:52 -0700623 final int minusSpace = negative ? 1 : 0;
Hans Boehm3666e632015-07-27 18:33:12 -0700624 final int msdIndex = truncated ? -1 : getNaiveMsdIndexOf(in); // INVALID_MSD is OK.
Hans Boehm5e802f32015-06-22 17:18:52 -0700625 String result = in;
Hans Boehm24c91ed2016-06-30 18:53:44 -0700626 boolean needEllipsis = false;
Hans Boehm73ecff22015-09-03 16:04:50 -0700627 if (truncated || (negative && result.charAt(0) != '-')) {
Hans Boehm24c91ed2016-06-30 18:53:44 -0700628 needEllipsis = true;
Hans Boehm73ecff22015-09-03 16:04:50 -0700629 result = KeyMaps.ELLIPSIS + result.substring(1, result.length());
630 // Ellipsis may be removed again in the type(1) scientific notation case.
631 }
632 final int decIndex = result.indexOf('.');
Hans Boehm65a99a42016-02-03 18:16:07 -0800633 if (lastDisplayedOffset != null) {
634 lastDisplayedOffset[0] = precOffset;
635 }
Hans Boehm24c91ed2016-06-30 18:53:44 -0700636 if (forceSciNotation || (decIndex == -1 || msdIndex != Evaluator.INVALID_MSD
Hans Boehm5e802f32015-06-22 17:18:52 -0700637 && msdIndex - decIndex > MAX_LEADING_ZEROES + 1) && precOffset != -1) {
Hans Boehm24c91ed2016-06-30 18:53:44 -0700638 // Either:
639 // 1) No decimal point displayed, and it's not just to the right of the last digit, or
640 // 2) we are at the front of a number whos integral part is too large to allow
641 // comma insertion, or
642 // 3) we should suppress leading zeroes.
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700643 // Add an exponent to let the user track which digits are currently displayed.
Hans Boehm5e802f32015-06-22 17:18:52 -0700644 // Start with type (2) exponent if we dropped no digits. -1 accounts for decimal point.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700645 // We currently never show digit separators together with an exponent.
Hans Boehm5e802f32015-06-22 17:18:52 -0700646 final int initExponent = precOffset > 0 ? -precOffset : -precOffset - 1;
647 int exponent = initExponent;
Hans Boehm08e8f322015-04-21 13:18:38 -0700648 boolean hasPoint = false;
Hans Boehm5e802f32015-06-22 17:18:52 -0700649 if (!truncated && msdIndex < maxDigs - 1
650 && result.length() - msdIndex + 1 + minusSpace
651 <= maxDigs + SCI_NOTATION_EXTRA) {
652 // Type (1) exponent computation and transformation:
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700653 // Leading digit is in display window. Use standard calculator scientific notation
654 // with one digit to the left of the decimal point. Insert decimal point and
655 // delete leading zeroes.
Hans Boehma0e45f32015-05-30 13:20:35 -0700656 // We try to keep leading digits roughly in position, and never
Hans Boehmf6dae112015-06-18 17:57:50 -0700657 // lengthen the result by more than SCI_NOTATION_EXTRA.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700658 if (decIndex > msdIndex) {
659 // In the forceSciNotation, we can have a decimal point in the relevant digit
660 // range. Remove it.
661 result = result.substring(0, decIndex)
662 + result.substring(decIndex + 1, result.length());
663 // msdIndex and precOffset unaffected.
664 }
Hans Boehm5e802f32015-06-22 17:18:52 -0700665 final int resLen = result.length();
666 String fraction = result.substring(msdIndex + 1, resLen);
667 result = (negative ? "-" : "") + result.substring(msdIndex, msdIndex + 1)
668 + "." + fraction;
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700669 // Original exp was correct for decimal point at right of fraction.
670 // Adjust by length of fraction.
Hans Boehm5e802f32015-06-22 17:18:52 -0700671 exponent = initExponent + resLen - msdIndex - 1;
Hans Boehm08e8f322015-04-21 13:18:38 -0700672 hasPoint = true;
673 }
Hans Boehm73ecff22015-09-03 16:04:50 -0700674 // Exponent can't be zero.
675 // Actually add the exponent of either type:
676 if (!forcePrecision) {
677 int dropDigits; // Digits to drop to make room for exponent.
678 if (hasPoint) {
679 // Type (1) exponent.
680 // Drop digits even if there is room. Otherwise the scrolling gets jumpy.
681 dropDigits = expLen(exponent);
682 if (dropDigits >= result.length() - 1) {
683 // Jumpy is better than no mantissa. Probably impossible anyway.
684 dropDigits = Math.max(result.length() - 2, 0);
Hans Boehma0e45f32015-05-30 13:20:35 -0700685 }
Hans Boehm73ecff22015-09-03 16:04:50 -0700686 } else {
687 // Type (2) exponent.
688 // Exponent depends on the number of digits we drop, which depends on
689 // exponent ...
690 for (dropDigits = 2; expLen(initExponent + dropDigits) > dropDigits;
691 ++dropDigits) {}
692 exponent = initExponent + dropDigits;
693 if (precOffset - dropDigits > mLsdOffset) {
694 // This can happen if e.g. result = 10^40 + 10^10
695 // It turns out we would otherwise display ...10e9 because it takes
696 // the same amount of space as ...1e10 but shows one more digit.
697 // But we don't want to display a trailing zero, even if it's free.
698 ++dropDigits;
699 ++exponent;
700 }
Hans Boehm08e8f322015-04-21 13:18:38 -0700701 }
Hans Boehm73ecff22015-09-03 16:04:50 -0700702 result = result.substring(0, result.length() - dropDigits);
Hans Boehm65a99a42016-02-03 18:16:07 -0800703 if (lastDisplayedOffset != null) {
704 lastDisplayedOffset[0] -= dropDigits;
705 }
Hans Boehm73ecff22015-09-03 16:04:50 -0700706 }
707 result = result + "E" + Integer.toString(exponent);
Hans Boehm24c91ed2016-06-30 18:53:44 -0700708 } else if (insertCommas) {
709 // Add commas to the whole number section, and then truncate on left to fit,
710 // counting commas as a fractional digit.
711 final int wholeStart = needEllipsis ? 1 : 0;
712 int orig_length = result.length();
713 final float nCommaChars;
714 if (decIndex != -1) {
715 nCommaChars = separatorChars(result, decIndex);
716 result = StringUtils.addCommas(result, wholeStart, decIndex)
717 + result.substring(decIndex, orig_length);
718 } else {
719 nCommaChars = separatorChars(result, orig_length);
720 result = StringUtils.addCommas(result, wholeStart, orig_length);
721 }
722 if (needEllipsis) {
723 orig_length -= 1; // Exclude ellipsis.
724 }
725 final float len = orig_length + nCommaChars;
726 int deletedChars = 0;
727 final float ellipsisCredit = getNoEllipsisCredit();
728 final float decimalCredit = getDecimalCredit();
729 final float effectiveLen = len - (decIndex == -1 ? 0 : getDecimalCredit());
730 final float ellipsisAdjustment =
731 needEllipsis ? mNoExponentCredit : getNoEllipsisCredit();
732 // As above, we allow for a tiny amount of extra length here, for consistency with
733 // getPreferredPrec().
734 if (effectiveLen - ellipsisAdjustment > (float) (maxDigs - wholeStart) + 0.0001f
735 && !forcePrecision) {
736 float deletedWidth = 0.0f;
737 while (effectiveLen - mNoExponentCredit - deletedWidth
738 > (float) (maxDigs - 1 /* for ellipsis */)) {
739 if (result.charAt(deletedChars) == ',') {
740 deletedWidth += mGroupingSeparatorWidthRatio;
741 } else {
742 deletedWidth += 1.0f;
743 }
744 deletedChars++;
745 }
746 }
747 if (deletedChars > 0) {
748 result = KeyMaps.ELLIPSIS + result.substring(deletedChars, result.length());
749 } else if (needEllipsis) {
750 result = KeyMaps.ELLIPSIS + result;
751 }
Hans Boehm5e802f32015-06-22 17:18:52 -0700752 }
753 return result;
Hans Boehm08e8f322015-04-21 13:18:38 -0700754 }
755
Hans Boehmf6dae112015-06-18 17:57:50 -0700756 /**
757 * Get formatted, but not internationalized, result from mEvaluator.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700758 * @param precOffset requested position (1 = tenths) of last included digit
759 * @param maxSize maximum number of characters (more or less) in result
760 * @param lastDisplayedOffset zeroth entry is set to actual offset of last included digit,
Hans Boehm65a99a42016-02-03 18:16:07 -0800761 * after adjusting for exponent, etc. May be null.
Hans Boehmf6dae112015-06-18 17:57:50 -0700762 * @param forcePrecision Ensure that last included digit is at pos, at the expense
763 * of treating maxSize as a soft limit.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700764 * @param forceSciNotation Force scientific notation, even if not required by maxSize.
765 * @param insertCommas Insert commas as digit separators.
Hans Boehmf6dae112015-06-18 17:57:50 -0700766 */
Hans Boehm5e802f32015-06-22 17:18:52 -0700767 private String getFormattedResult(int precOffset, int maxSize, int lastDisplayedOffset[],
Hans Boehm24c91ed2016-06-30 18:53:44 -0700768 boolean forcePrecision, boolean forceSciNotation, boolean insertCommas) {
Hans Boehm08e8f322015-04-21 13:18:38 -0700769 final boolean truncated[] = new boolean[1];
770 final boolean negative[] = new boolean[1];
Hans Boehm5e802f32015-06-22 17:18:52 -0700771 final int requestedPrecOffset[] = {precOffset};
Hans Boehm8f051c32016-10-03 16:53:58 -0700772 final String rawResult = mEvaluator.getString(mIndex, requestedPrecOffset, mMaxCharOffset,
773 maxSize, truncated, negative, this);
Hans Boehm5e802f32015-06-22 17:18:52 -0700774 return formatResult(rawResult, requestedPrecOffset[0], maxSize, truncated[0], negative[0],
Hans Boehm24c91ed2016-06-30 18:53:44 -0700775 lastDisplayedOffset, forcePrecision, forceSciNotation, insertCommas);
Hans Boehm08e8f322015-04-21 13:18:38 -0700776 }
777
Hans Boehm65a99a42016-02-03 18:16:07 -0800778 /**
779 * Return entire result (within reason) up to current displayed precision.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700780 * @param withSeparators Add digit separators
Hans Boehm65a99a42016-02-03 18:16:07 -0800781 */
Hans Boehm24c91ed2016-06-30 18:53:44 -0700782 public String getFullText(boolean withSeparators) {
Hans Boehm760a9dc2015-04-20 10:27:12 -0700783 if (!mValid) return "";
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700784 if (!mScrollable) return getText().toString();
Hans Boehm5e802f32015-06-22 17:18:52 -0700785 return KeyMaps.translateResult(getFormattedResult(mLastDisplayedOffset, MAX_COPY_SIZE,
Hans Boehm24c91ed2016-06-30 18:53:44 -0700786 null, true /* forcePrecision */, false /* forceSciNotation */, withSeparators));
Hans Boehm84614952014-11-25 18:46:17 -0800787 }
788
Hans Boehm24c91ed2016-06-30 18:53:44 -0700789 /**
790 * Did the above produce a correct result?
791 * UI thread only.
792 */
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700793 public boolean fullTextIsExact() {
Hans Boehm24c91ed2016-06-30 18:53:44 -0700794 return !mScrollable || (mMaxCharOffset == getCharOffset(mCurrentPos)
795 && mMaxCharOffset != MAX_RIGHT_SCROLL);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700796 }
797
Hans Boehm61568a12015-05-18 18:25:41 -0700798 /**
Hans Boehm65a99a42016-02-03 18:16:07 -0800799 * Get entire result up to current displayed precision, or up to MAX_COPY_EXTRA additional
800 * digits, if it will lead to an exact result.
801 */
802 public String getFullCopyText() {
803 if (!mValid
804 || mLsdOffset == Integer.MAX_VALUE
805 || fullTextIsExact()
806 || mWholeLen > MAX_RECOMPUTE_DIGITS
807 || mWholeLen + mLsdOffset > MAX_RECOMPUTE_DIGITS
808 || mLsdOffset - mLastDisplayedOffset > MAX_COPY_EXTRA) {
Hans Boehm24c91ed2016-06-30 18:53:44 -0700809 return getFullText(false /* withSeparators */);
Hans Boehm65a99a42016-02-03 18:16:07 -0800810 }
811 // It's reasonable to compute and copy the exact result instead.
812 final int nonNegLsdOffset = Math.max(0, mLsdOffset);
Hans Boehm8f051c32016-10-03 16:53:58 -0700813 final String rawResult = mEvaluator.getResult(mIndex).toStringTruncated(nonNegLsdOffset);
Hans Boehm65a99a42016-02-03 18:16:07 -0800814 final String formattedResult = formatResult(rawResult, nonNegLsdOffset, MAX_COPY_SIZE,
Hans Boehm24c91ed2016-06-30 18:53:44 -0700815 false, rawResult.charAt(0) == '-', null, true /* forcePrecision */,
816 false /* forceSciNotation */, false /* insertCommas */);
Hans Boehm65a99a42016-02-03 18:16:07 -0800817 return KeyMaps.translateResult(formattedResult);
818 }
819
820 /**
Hans Boehm61568a12015-05-18 18:25:41 -0700821 * Return the maximum number of characters that will fit in the result display.
Hans Boehm8f051c32016-10-03 16:53:58 -0700822 * May be called asynchronously from non-UI thread. From Evaluator.CharMetricsInfo.
Hans Boehmd4959e82016-11-15 18:01:28 -0800823 * Returns zero if measurement hasn't completed.
Hans Boehm61568a12015-05-18 18:25:41 -0700824 */
Hans Boehm8f051c32016-10-03 16:53:58 -0700825 @Override
826 public int getMaxChars() {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700827 int result;
828 synchronized(mWidthLock) {
Hans Boehmd4959e82016-11-15 18:01:28 -0800829 return (int) Math.floor(mWidthConstraint / mCharWidth);
Hans Boehm84614952014-11-25 18:46:17 -0800830 }
831 }
832
Hans Boehm61568a12015-05-18 18:25:41 -0700833 /**
Justin Klaassen44595162015-05-28 17:55:20 -0700834 * @return {@code true} if the currently displayed result is scrollable
Hans Boehm61568a12015-05-18 18:25:41 -0700835 */
Justin Klaassen44595162015-05-28 17:55:20 -0700836 public boolean isScrollable() {
837 return mScrollable;
Hans Boehm61568a12015-05-18 18:25:41 -0700838 }
839
Hans Boehm24c91ed2016-06-30 18:53:44 -0700840 /**
841 * Map pixel position to digit offset.
842 * UI thread only.
843 */
844 int getCharOffset(int pos) {
845 return (int) Math.round(pos / mCharWidth); // Lock not needed.
Hans Boehm013969e2015-04-13 20:29:47 -0700846 }
847
Hans Boehm84614952014-11-25 18:46:17 -0800848 void clear() {
Hans Boehm760a9dc2015-04-20 10:27:12 -0700849 mValid = false;
Hans Boehm1176f232015-05-11 16:26:03 -0700850 mScrollable = false;
Hans Boehm84614952014-11-25 18:46:17 -0800851 setText("");
Christine Franksafe28bb2016-07-29 17:24:52 -0700852 setLongClickable(false);
Hans Boehm84614952014-11-25 18:46:17 -0800853 }
854
Hans Boehm8f051c32016-10-03 16:53:58 -0700855 @Override
856 public void onCancelled(long index) {
857 clear();
Annie Chin37c33b62016-11-22 14:46:28 -0800858 mStoreToMemoryRequested = false;
Hans Boehm8f051c32016-10-03 16:53:58 -0700859 }
860
Hans Boehm24c91ed2016-06-30 18:53:44 -0700861 /**
862 * Refresh display.
Hans Boehm8f051c32016-10-03 16:53:58 -0700863 * Only called in UI thread. Index argument is currently ignored.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700864 */
Hans Boehm8f051c32016-10-03 16:53:58 -0700865 @Override
866 public void onReevaluate(long index) {
867 redisplay();
868 }
869
870 public void redisplay() {
Christine Franks6f6c24a2016-09-08 18:21:47 -0700871 if (mScroller.isFinished() && length() > 0) {
Christine Franksd21205c2016-08-04 10:06:15 -0700872 setAccessibilityLiveRegion(ACCESSIBILITY_LIVE_REGION_POLITE);
873 }
Hans Boehm24c91ed2016-06-30 18:53:44 -0700874 int currentCharOffset = getCharOffset(mCurrentPos);
Hans Boehm84614952014-11-25 18:46:17 -0800875 int maxChars = getMaxChars();
Hans Boehm5e802f32015-06-22 17:18:52 -0700876 int lastDisplayedOffset[] = new int[1];
Hans Boehm24c91ed2016-06-30 18:53:44 -0700877 String result = getFormattedResult(currentCharOffset, maxChars, lastDisplayedOffset,
878 mAppendExponent /* forcePrecision; preserve entire result */,
879 !mWholePartFits
880 && currentCharOffset == getCharOffset(mMinPos) /* forceSciNotation */,
881 mWholePartFits /* insertCommas */ );
Hans Boehm0b9806f2015-06-29 16:07:15 -0700882 int expIndex = result.indexOf('E');
Hans Boehm013969e2015-04-13 20:29:47 -0700883 result = KeyMaps.translateResult(result);
Hans Boehm5e802f32015-06-22 17:18:52 -0700884 if (expIndex > 0 && result.indexOf('.') == -1) {
Hans Boehm84614952014-11-25 18:46:17 -0800885 // Gray out exponent if used as position indicator
886 SpannableString formattedResult = new SpannableString(result);
Hans Boehm5e802f32015-06-22 17:18:52 -0700887 formattedResult.setSpan(mExponentColorSpan, expIndex, result.length(),
Hans Boehm84614952014-11-25 18:46:17 -0800888 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
889 setText(formattedResult);
890 } else {
891 setText(result);
892 }
Hans Boehm5e802f32015-06-22 17:18:52 -0700893 mLastDisplayedOffset = lastDisplayedOffset[0];
Hans Boehm760a9dc2015-04-20 10:27:12 -0700894 mValid = true;
Christine Franksafe28bb2016-07-29 17:24:52 -0700895 setLongClickable(true);
Hans Boehm84614952014-11-25 18:46:17 -0800896 }
897
898 @Override
Christine Franks6f6c24a2016-09-08 18:21:47 -0700899 protected void onTextChanged(java.lang.CharSequence text, int start, int lengthBefore,
900 int lengthAfter) {
901 super.onTextChanged(text, start, lengthBefore, lengthAfter);
902
903 if (!mScrollable || mScroller.isFinished()) {
904 if (lengthBefore == 0 && lengthAfter > 0) {
905 setAccessibilityLiveRegion(ACCESSIBILITY_LIVE_REGION_POLITE);
906 setContentDescription(null);
907 } else if (lengthBefore > 0 && lengthAfter == 0) {
908 setAccessibilityLiveRegion(ACCESSIBILITY_LIVE_REGION_NONE);
909 setContentDescription(getContext().getString(R.string.desc_result));
910 }
911 }
912 }
913
914 @Override
Hans Boehm84614952014-11-25 18:46:17 -0800915 public void computeScroll() {
Christine Franks6f6c24a2016-09-08 18:21:47 -0700916 if (!mScrollable) {
917 return;
918 }
919
Hans Boehm84614952014-11-25 18:46:17 -0800920 if (mScroller.computeScrollOffset()) {
921 mCurrentPos = mScroller.getCurrX();
Hans Boehm24c91ed2016-06-30 18:53:44 -0700922 if (getCharOffset(mCurrentPos) != getCharOffset(mLastPos)) {
Hans Boehm84614952014-11-25 18:46:17 -0800923 mLastPos = mCurrentPos;
924 redisplay();
925 }
Christine Franks6f6c24a2016-09-08 18:21:47 -0700926 }
927
928 if (!mScroller.isFinished()) {
Justin Klaassen44595162015-05-28 17:55:20 -0700929 postInvalidateOnAnimation();
Christine Franksd21205c2016-08-04 10:06:15 -0700930 setAccessibilityLiveRegion(ACCESSIBILITY_LIVE_REGION_NONE);
Christine Franks6f6c24a2016-09-08 18:21:47 -0700931 } else if (length() > 0){
Christine Franksd21205c2016-08-04 10:06:15 -0700932 setAccessibilityLiveRegion(ACCESSIBILITY_LIVE_REGION_POLITE);
Hans Boehm84614952014-11-25 18:46:17 -0800933 }
934 }
935
Chenjie Yu3937b652016-06-01 23:14:26 -0700936 /**
Christine Franks1d99be12016-11-14 14:00:36 -0800937 * Use ActionMode for copy/memory support on M and higher.
Chenjie Yu3937b652016-06-01 23:14:26 -0700938 */
939 @TargetApi(Build.VERSION_CODES.M)
940 private void setupActionMode() {
941 mCopyActionModeCallback = new ActionMode.Callback2() {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700942
Chenjie Yu3937b652016-06-01 23:14:26 -0700943 @Override
944 public boolean onCreateActionMode(ActionMode mode, Menu menu) {
945 final MenuInflater inflater = mode.getMenuInflater();
946 return createCopyMenu(inflater, menu);
947 }
Hans Boehm7f83e362015-06-10 15:41:04 -0700948
Chenjie Yu3937b652016-06-01 23:14:26 -0700949 @Override
950 public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
951 return false; // Return false if nothing is done
952 }
Hans Boehm7f83e362015-06-10 15:41:04 -0700953
Chenjie Yu3937b652016-06-01 23:14:26 -0700954 @Override
955 public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
956 if (onMenuItemClick(item)) {
Hans Boehm65a99a42016-02-03 18:16:07 -0800957 mode.finish();
958 return true;
Chenjie Yu3937b652016-06-01 23:14:26 -0700959 } else {
960 return false;
Hans Boehm65a99a42016-02-03 18:16:07 -0800961 }
Chenjie Yu3937b652016-06-01 23:14:26 -0700962 }
963
964 @Override
965 public void onDestroyActionMode(ActionMode mode) {
966 unhighlightResult();
967 mActionMode = null;
968 }
969
970 @Override
971 public void onGetContentRect(ActionMode mode, View view, Rect outRect) {
972 super.onGetContentRect(mode, view, outRect);
973
974 outRect.left += view.getPaddingLeft();
975 outRect.top += view.getPaddingTop();
976 outRect.right -= view.getPaddingRight();
977 outRect.bottom -= view.getPaddingBottom();
978 final int width = (int) Layout.getDesiredWidth(getText(), getPaint());
979 if (width < outRect.width()) {
980 outRect.left = outRect.right - width;
981 }
982
983 if (!BuildCompat.isAtLeastN()) {
984 // The CAB (prior to N) only takes the translation of a view into account, so
985 // if a scale is applied to the view then the offset outRect will end up being
986 // positioned incorrectly. We workaround that limitation by manually applying
987 // the scale to the outRect, which the CAB will then offset to the correct
988 // position.
989 final float scaleX = view.getScaleX();
990 final float scaleY = view.getScaleY();
991 outRect.left *= scaleX;
992 outRect.right *= scaleX;
993 outRect.top *= scaleY;
994 outRect.bottom *= scaleY;
995 }
996 }
997 };
998 setOnLongClickListener(new View.OnLongClickListener() {
999 @Override
1000 public boolean onLongClick(View v) {
1001 if (mValid) {
1002 mActionMode = startActionMode(mCopyActionModeCallback,
1003 ActionMode.TYPE_FLOATING);
1004 return true;
1005 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001006 return false;
1007 }
Chenjie Yu3937b652016-06-01 23:14:26 -07001008 });
1009 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001010
Chenjie Yu3937b652016-06-01 23:14:26 -07001011 /**
Christine Franks1d99be12016-11-14 14:00:36 -08001012 * Use ContextMenu for copy/memory support on L and lower.
Chenjie Yu3937b652016-06-01 23:14:26 -07001013 */
1014 private void setupContextMenu() {
1015 setOnCreateContextMenuListener(new OnCreateContextMenuListener() {
1016 @Override
1017 public void onCreateContextMenu(ContextMenu contextMenu, View view,
1018 ContextMenu.ContextMenuInfo contextMenuInfo) {
1019 final MenuInflater inflater = new MenuInflater(getContext());
1020 createCopyMenu(inflater, contextMenu);
1021 mContextMenu = contextMenu;
1022 for(int i = 0; i < contextMenu.size(); i ++) {
1023 contextMenu.getItem(i).setOnMenuItemClickListener(CalculatorResult.this);
1024 }
Hans Boehm7f83e362015-06-10 15:41:04 -07001025 }
Chenjie Yu3937b652016-06-01 23:14:26 -07001026 });
1027 setOnLongClickListener(new View.OnLongClickListener() {
1028 @Override
1029 public boolean onLongClick(View v) {
1030 if (mValid) {
1031 return showContextMenu();
1032 }
1033 return false;
Justin Klaassenf1b61f42016-04-27 16:00:11 -07001034 }
Chenjie Yu3937b652016-06-01 23:14:26 -07001035 });
1036 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001037
Chenjie Yu3937b652016-06-01 23:14:26 -07001038 private boolean createCopyMenu(MenuInflater inflater, Menu menu) {
Christine Franks1d99be12016-11-14 14:00:36 -08001039 inflater.inflate(R.menu.menu_result, menu);
1040 final boolean displayMemory = mEvaluator.getMemoryIndex() != 0;
1041 final MenuItem memoryAddItem = menu.findItem(R.id.memory_add);
1042 final MenuItem memorySubtractItem = menu.findItem(R.id.memory_subtract);
1043 memoryAddItem.setEnabled(displayMemory);
1044 memorySubtractItem.setEnabled(displayMemory);
Chenjie Yu3937b652016-06-01 23:14:26 -07001045 highlightResult();
1046 return true;
1047 }
1048
1049 public boolean stopActionModeOrContextMenu() {
Hans Boehm1176f232015-05-11 16:26:03 -07001050 if (mActionMode != null) {
1051 mActionMode.finish();
1052 return true;
1053 }
Chenjie Yu3937b652016-06-01 23:14:26 -07001054 if (mContextMenu != null) {
1055 unhighlightResult();
1056 mContextMenu.close();
1057 return true;
1058 }
Hans Boehm1176f232015-05-11 16:26:03 -07001059 return false;
1060 }
1061
Chenjie Yu3937b652016-06-01 23:14:26 -07001062 private void highlightResult() {
1063 final Spannable text = (Spannable) getText();
1064 text.setSpan(mHighlightSpan, 0, text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
1065 }
1066
1067 private void unhighlightResult() {
1068 final Spannable text = (Spannable) getText();
1069 text.removeSpan(mHighlightSpan);
1070 }
1071
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001072 private void setPrimaryClip(ClipData clip) {
1073 ClipboardManager clipboard = (ClipboardManager) getContext().
Hans Boehmc01cd7f2015-05-12 18:32:19 -07001074 getSystemService(Context.CLIPBOARD_SERVICE);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001075 clipboard.setPrimaryClip(clip);
1076 }
1077
1078 private void copyContent() {
Hans Boehm65a99a42016-02-03 18:16:07 -08001079 final CharSequence text = getFullCopyText();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001080 ClipboardManager clipboard =
Hans Boehmc01cd7f2015-05-12 18:32:19 -07001081 (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
1082 // We include a tag URI, to allow us to recognize our own results and handle them
1083 // specially.
Hans Boehm8f051c32016-10-03 16:53:58 -07001084 ClipData.Item newItem = new ClipData.Item(text, null, mEvaluator.capture(mIndex));
Hans Boehmc01cd7f2015-05-12 18:32:19 -07001085 String[] mimeTypes = new String[] {ClipDescription.MIMETYPE_TEXT_PLAIN};
1086 ClipData cd = new ClipData("calculator result", mimeTypes, newItem);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001087 clipboard.setPrimaryClip(cd);
Hans Boehmc01cd7f2015-05-12 18:32:19 -07001088 Toast.makeText(getContext(), R.string.text_copied_toast, Toast.LENGTH_SHORT).show();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001089 }
1090
Chenjie Yu3937b652016-06-01 23:14:26 -07001091 @Override
1092 public boolean onMenuItemClick(MenuItem item) {
1093 switch (item.getItemId()) {
Christine Franks1d99be12016-11-14 14:00:36 -08001094 case R.id.memory_add:
1095 onMemoryAdd();
1096 return true;
1097 case R.id.memory_subtract:
1098 onMemorySubtract();
1099 return true;
1100 case R.id.memory_store:
1101 onMemoryStore();
1102 return true;
Chenjie Yu3937b652016-06-01 23:14:26 -07001103 case R.id.menu_copy:
Hans Boehm8f051c32016-10-03 16:53:58 -07001104 if (mEvaluator.evaluationInProgress(mIndex)) {
Chenjie Yu3937b652016-06-01 23:14:26 -07001105 // Refuse to copy placeholder characters.
1106 return false;
1107 } else {
1108 copyContent();
1109 unhighlightResult();
1110 return true;
1111 }
1112 default:
1113 return false;
1114 }
1115 }
Christine Franks1d99be12016-11-14 14:00:36 -08001116}