blob: bfabfcef0ffcebfdc4030f0d2d653241639b887b [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 Boehm4a6b7cb2015-04-03 18:41:52 -0700101 private int mWidthConstraint = -1;
Hans Boehma0e45f32015-05-30 13:20:35 -0700102 // Our total width in pixels minus space for ellipsis.
Justin Klaassen44595162015-05-28 17:55:20 -0700103 private float mCharWidth = 1;
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700104 // Maximum character width. For now we pretend that all characters
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700105 // have this width.
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700106 // TODO: We're not really using a fixed width font. But it appears
107 // to be close enough for the characters we use that the difference
108 // is not noticeable.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700109 private float mGroupingSeparatorWidthRatio;
110 // Fraction of digit width occupied by a digit separator.
111 private float mDecimalCredit;
112 // Fraction of digit width saved by replacing digit with decimal point.
113 private float mNoEllipsisCredit;
114 // Fraction of digit width saved by both replacing ellipsis with digit
115 // and avoiding scientific notation.
Annie Chinbc001882016-11-09 19:41:21 -0800116 private boolean mShouldRequireResult = true;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700117 private static final int MAX_WIDTH = 100;
Hans Boehm24c91ed2016-06-30 18:53:44 -0700118 // Maximum number of digits displayed.
Hans Boehm50ed3202015-06-09 14:35:49 -0700119 public static final int MAX_LEADING_ZEROES = 6;
Hans Boehma0e45f32015-05-30 13:20:35 -0700120 // Maximum number of leading zeroes after decimal point before we
121 // switch to scientific notation with negative exponent.
Hans Boehm50ed3202015-06-09 14:35:49 -0700122 public static final int MAX_TRAILING_ZEROES = 6;
Hans Boehma0e45f32015-05-30 13:20:35 -0700123 // Maximum number of trailing zeroes before the decimal point before
124 // we switch to scientific notation with positive exponent.
125 private static final int SCI_NOTATION_EXTRA = 1;
126 // Extra digits for standard scientific notation. In this case we
Hans Boehm80018c82015-08-02 16:59:07 -0700127 // have a decimal point and no ellipsis.
128 // We assume that we do not drop digits to make room for the decimal
129 // point in ordinary scientific notation. Thus >= 1.
Hans Boehm65a99a42016-02-03 18:16:07 -0800130 private static final int MAX_COPY_EXTRA = 100;
131 // The number of extra digits we are willing to compute to copy
132 // a result as an exact number.
133 private static final int MAX_RECOMPUTE_DIGITS = 2000;
134 // The maximum number of digits we're willing to recompute in the UI
135 // thread. We only do this for known rational results, where we
136 // can bound the computation cost.
Chenjie Yu3937b652016-06-01 23:14:26 -0700137 private final ForegroundColorSpan mExponentColorSpan;
138 private final BackgroundColorSpan mHighlightSpan;
Hans Boehm65a99a42016-02-03 18:16:07 -0800139
Hans Boehm1176f232015-05-11 16:26:03 -0700140 private ActionMode mActionMode;
Chenjie Yu3937b652016-06-01 23:14:26 -0700141 private ActionMode.Callback mCopyActionModeCallback;
142 private ContextMenu mContextMenu;
Hans Boehm84614952014-11-25 18:46:17 -0800143
144 public CalculatorResult(Context context, AttributeSet attrs) {
145 super(context, attrs);
146 mScroller = new OverScroller(context);
Chenjie Yu3937b652016-06-01 23:14:26 -0700147 mHighlightSpan = new BackgroundColorSpan(getHighlightColor());
148 mExponentColorSpan = new ForegroundColorSpan(
149 ContextCompat.getColor(context, R.color.display_result_exponent_text_color));
Hans Boehm84614952014-11-25 18:46:17 -0800150 mGestureDetector = new GestureDetector(context,
151 new GestureDetector.SimpleOnGestureListener() {
152 @Override
Justin Klaassend48b7562015-04-16 16:51:38 -0700153 public boolean onDown(MotionEvent e) {
154 return true;
155 }
156 @Override
Hans Boehm84614952014-11-25 18:46:17 -0800157 public boolean onFling(MotionEvent e1, MotionEvent e2,
158 float velocityX, float velocityY) {
159 if (!mScroller.isFinished()) {
160 mCurrentPos = mScroller.getFinalX();
161 }
162 mScroller.forceFinished(true);
Chenjie Yu3937b652016-06-01 23:14:26 -0700163 stopActionModeOrContextMenu();
Hans Boehmfbcef702015-04-27 18:07:47 -0700164 CalculatorResult.this.cancelLongPress();
165 // Ignore scrolls of error string, etc.
166 if (!mScrollable) return true;
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700167 mScroller.fling(mCurrentPos, 0, - (int) velocityX, 0 /* horizontal only */,
Hans Boehm61568a12015-05-18 18:25:41 -0700168 mMinPos, mMaxPos, 0, 0);
Justin Klaassen44595162015-05-28 17:55:20 -0700169 postInvalidateOnAnimation();
Hans Boehm84614952014-11-25 18:46:17 -0800170 return true;
171 }
172 @Override
173 public boolean onScroll(MotionEvent e1, MotionEvent e2,
174 float distanceX, float distanceY) {
Hans Boehm61568a12015-05-18 18:25:41 -0700175 int distance = (int)distanceX;
Hans Boehm84614952014-11-25 18:46:17 -0800176 if (!mScroller.isFinished()) {
177 mCurrentPos = mScroller.getFinalX();
178 }
179 mScroller.forceFinished(true);
Chenjie Yu3937b652016-06-01 23:14:26 -0700180 stopActionModeOrContextMenu();
Hans Boehm84614952014-11-25 18:46:17 -0800181 CalculatorResult.this.cancelLongPress();
182 if (!mScrollable) return true;
Hans Boehm61568a12015-05-18 18:25:41 -0700183 if (mCurrentPos + distance < mMinPos) {
184 distance = mMinPos - mCurrentPos;
185 } else if (mCurrentPos + distance > mMaxPos) {
186 distance = mMaxPos - mCurrentPos;
187 }
Hans Boehm84614952014-11-25 18:46:17 -0800188 int duration = (int)(e2.getEventTime() - e1.getEventTime());
189 if (duration < 1 || duration > 100) duration = 10;
Hans Boehm61568a12015-05-18 18:25:41 -0700190 mScroller.startScroll(mCurrentPos, 0, distance, 0, (int)duration);
Justin Klaassen44595162015-05-28 17:55:20 -0700191 postInvalidateOnAnimation();
Hans Boehm84614952014-11-25 18:46:17 -0800192 return true;
193 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700194 @Override
195 public void onLongPress(MotionEvent e) {
Hans Boehm1176f232015-05-11 16:26:03 -0700196 if (mValid) {
Justin Klaassen3a05c7e2016-03-04 12:40:02 -0800197 performLongClick();
Hans Boehm1176f232015-05-11 16:26:03 -0700198 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700199 }
Hans Boehm84614952014-11-25 18:46:17 -0800200 });
Justin Klaassen3a05c7e2016-03-04 12:40:02 -0800201 setOnTouchListener(new View.OnTouchListener() {
202 @Override
203 public boolean onTouch(View v, MotionEvent event) {
204 return mGestureDetector.onTouchEvent(event);
205 }
206 });
Hans Boehm14344ff2016-06-08 13:01:51 -0700207
Chenjie Yu3937b652016-06-01 23:14:26 -0700208 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
209 setupActionMode();
210 } else {
211 setupContextMenu();
212 }
Hans Boehm14344ff2016-06-08 13:01:51 -0700213
Hans Boehm84614952014-11-25 18:46:17 -0800214 setCursorVisible(false);
Christine Franksafe28bb2016-07-29 17:24:52 -0700215 setLongClickable(false);
Christine Franks6f6c24a2016-09-08 18:21:47 -0700216 setContentDescription(context.getString(R.string.desc_result));
Hans Boehm84614952014-11-25 18:46:17 -0800217 }
218
Hans Boehm8f051c32016-10-03 16:53:58 -0700219 void setEvaluator(Evaluator evaluator, long index) {
Hans Boehm84614952014-11-25 18:46:17 -0800220 mEvaluator = evaluator;
Hans Boehm8f051c32016-10-03 16:53:58 -0700221 mIndex = index;
Hans Boehm84614952014-11-25 18:46:17 -0800222 }
223
Hans Boehmcd72f7e2016-06-01 16:21:25 -0700224 // Compute maximum digit width the hard way.
225 private static float getMaxDigitWidth(TextPaint paint) {
226 // Compute the maximum advance width for each digit, thus accounting for between-character
227 // spaces. If we ever support other kinds of digits, we may have to avoid kerning effects
228 // that could reduce the advance width within this particular string.
229 final String allDigits = "0123456789";
230 final float[] widths = new float[allDigits.length()];
231 paint.getTextWidths(allDigits, widths);
232 float maxWidth = 0;
233 for (float x : widths) {
234 maxWidth = Math.max(x, maxWidth);
235 }
236 return maxWidth;
237 }
238
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700239 @Override
240 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Justin Klaassend06f51d2016-08-03 00:41:31 -0700241 if (!isLaidOut()) {
242 // Set a minimum height so scaled error messages won't affect our layout.
243 setMinimumHeight(getLineHeight() + getCompoundPaddingBottom()
244 + getCompoundPaddingTop());
245 }
246
Justin Klaassen44595162015-05-28 17:55:20 -0700247 final TextPaint paint = getPaint();
Hans Boehm80018c82015-08-02 16:59:07 -0700248 final Context context = getContext();
Hans Boehmcd72f7e2016-06-01 16:21:25 -0700249 final float newCharWidth = getMaxDigitWidth(paint);
Hans Boehm80018c82015-08-02 16:59:07 -0700250 // Digits are presumed to have no more than newCharWidth.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700251 // There are two instances when we know that the result is otherwise narrower than
252 // expected:
253 // 1. For standard scientific notation (our type 1), we know that we have a norrow decimal
254 // point and no (usually wide) ellipsis symbol. We allow one extra digit
255 // (SCI_NOTATION_EXTRA) to compensate, and consider that in determining available width.
256 // 2. If we are using digit grouping separators and a decimal point, we give ourselves
257 // a fractional extra space for those separators, the value of which depends on whether
258 // there is also an ellipsis.
259 //
260 // Maximum extra space we need in various cases:
261 // Type 1 scientific notation, assuming ellipsis, minus sign and E are wider than a digit:
262 // Two minus signs + "E" + "." - 3 digits.
263 // Type 2 scientific notation:
264 // Ellipsis + "E" + "-" - 3 digits.
265 // In the absence of scientific notation, we may need a little less space.
266 // We give ourselves a bit of extra credit towards comma insertion and give
267 // ourselves more if we have either
268 // No ellipsis, or
269 // A decimal separator.
270
271 // Calculate extra space we need to reserve, in addition to character count.
Hans Boehm80018c82015-08-02 16:59:07 -0700272 final float decimalSeparatorWidth = Layout.getDesiredWidth(
273 context.getString(R.string.dec_point), paint);
Hans Boehm24c91ed2016-06-30 18:53:44 -0700274 final float minusWidth = Layout.getDesiredWidth(context.getString(R.string.op_sub), paint);
275 final float minusExtraWidth = Math.max(minusWidth - newCharWidth, 0.0f);
276 final float ellipsisWidth = Layout.getDesiredWidth(KeyMaps.ELLIPSIS, paint);
277 final float ellipsisExtraWidth = Math.max(ellipsisWidth - newCharWidth, 0.0f);
278 final float expWidth = Layout.getDesiredWidth(KeyMaps.translateResult("e"), paint);
279 final float expExtraWidth = Math.max(expWidth - newCharWidth, 0.0f);
280 final float type1Extra = 2 * minusExtraWidth + expExtraWidth + decimalSeparatorWidth;
281 final float type2Extra = ellipsisExtraWidth + expExtraWidth + minusExtraWidth;
282 final float extraWidth = Math.max(type1Extra, type2Extra);
283 final int intExtraWidth = (int) Math.ceil(extraWidth) + 1 /* to cover rounding sins */;
Hans Boehm80018c82015-08-02 16:59:07 -0700284 final int newWidthConstraint = MeasureSpec.getSize(widthMeasureSpec)
Hans Boehm24c91ed2016-06-30 18:53:44 -0700285 - (getPaddingLeft() + getPaddingRight()) - intExtraWidth;
286
287 // Calculate other width constants we need to handle grouping separators.
288 final float groupingSeparatorW =
289 Layout.getDesiredWidth(KeyMaps.translateResult(","), paint);
290 // Credits in the absence of any scientific notation:
291 float noExponentCredit = extraWidth - Math.max(ellipsisExtraWidth, minusExtraWidth);
292 final float noEllipsisCredit = extraWidth - minusExtraWidth; // includes noExponentCredit.
293 final float decimalCredit = Math.max(newCharWidth - decimalSeparatorWidth, 0.0f);
294
295 mNoExponentCredit = noExponentCredit / newCharWidth;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700296 synchronized(mWidthLock) {
Hans Boehm013969e2015-04-13 20:29:47 -0700297 mWidthConstraint = newWidthConstraint;
298 mCharWidth = newCharWidth;
Hans Boehm24c91ed2016-06-30 18:53:44 -0700299 mNoEllipsisCredit = noEllipsisCredit / newCharWidth;
300 mDecimalCredit = decimalCredit / newCharWidth;
301 mGroupingSeparatorWidthRatio = groupingSeparatorW / newCharWidth;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700302 }
Hans Boehm14344ff2016-06-08 13:01:51 -0700303
304 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700305 }
306
Annie Chin06fd3cf2016-11-07 16:04:33 -0800307 @Override
308 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
309 super.onLayout(changed, left, top, right, bottom);
310
Annie Chinbc001882016-11-09 19:41:21 -0800311 if (mEvaluator != null && mShouldRequireResult) {
Annie Chin06fd3cf2016-11-07 16:04:33 -0800312 final CalculatorExpr expr = mEvaluator.getExpr(mIndex);
313 if (expr != null && expr.hasInterestingOps()) {
314 mEvaluator.requireResult(mIndex, this, this);
315 }
316 }
317 }
318
Annie Chinbc001882016-11-09 19:41:21 -0800319 public void setShouldRequireResult(boolean should) {
320 mShouldRequireResult = should;
321 }
322
Hans Boehm8f051c32016-10-03 16:53:58 -0700323 // From Evaluator.CharMetricsInfo.
324 @Override
Hans Boehm24c91ed2016-06-30 18:53:44 -0700325 public float separatorChars(String s, int len) {
326 int start = 0;
327 while (start < len && !Character.isDigit(s.charAt(start))) {
328 ++start;
329 }
330 // We assume the rest consists of digits, and for consistency with the rest
331 // of the code, we assume all digits have width mCharWidth.
332 final int nDigits = len - start;
333 // We currently insert a digit separator every three digits.
334 final int nSeparators = (nDigits - 1) / 3;
335 synchronized(mWidthLock) {
336 // Always return an upper bound, even in the presence of rounding errors.
337 return nSeparators * mGroupingSeparatorWidthRatio;
338 }
339 }
340
Hans Boehm8f051c32016-10-03 16:53:58 -0700341 // From Evaluator.CharMetricsInfo.
342 @Override
Hans Boehm24c91ed2016-06-30 18:53:44 -0700343 public float getNoEllipsisCredit() {
344 synchronized(mWidthLock) {
345 return mNoEllipsisCredit;
346 }
347 }
348
Hans Boehm8f051c32016-10-03 16:53:58 -0700349 // From Evaluator.CharMetricsInfo.
350 @Override
Hans Boehm24c91ed2016-06-30 18:53:44 -0700351 public float getDecimalCredit() {
352 synchronized(mWidthLock) {
353 return mDecimalCredit;
354 }
355 }
356
Hans Boehma0e45f32015-05-30 13:20:35 -0700357 // Return the length of the exponent representation for the given exponent, in
358 // characters.
359 private final int expLen(int exp) {
360 if (exp == 0) return 0;
Hans Boehm5e802f32015-06-22 17:18:52 -0700361 final int abs_exp_digits = (int) Math.ceil(Math.log10(Math.abs((double)exp))
362 + 0.0000000001d /* Round whole numbers to next integer */);
363 return abs_exp_digits + (exp >= 0 ? 1 : 2);
Hans Boehm61568a12015-05-18 18:25:41 -0700364 }
365
Hans Boehma0e45f32015-05-30 13:20:35 -0700366 /**
367 * Initiate display of a new result.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700368 * Only called from UI thread.
Hans Boehma0e45f32015-05-30 13:20:35 -0700369 * The parameters specify various properties of the result.
Hans Boehm8f051c32016-10-03 16:53:58 -0700370 * @param index Index of expression that was just evaluated. Currently ignored, since we only
371 * expect notification for the expression result being displayed.
Hans Boehma0e45f32015-05-30 13:20:35 -0700372 * @param initPrec Initial display precision computed by evaluator. (1 = tenths digit)
373 * @param msd Position of most significant digit. Offset from left of string.
374 Evaluator.INVALID_MSD if unknown.
375 * @param leastDigPos Position of least significant digit (1 = tenths digit)
376 * or Integer.MAX_VALUE.
377 * @param truncatedWholePart Result up to but not including decimal point.
378 Currently we only use the length.
379 */
Hans Boehm8f051c32016-10-03 16:53:58 -0700380 @Override
381 public void onEvaluate(long index, int initPrec, int msd, int leastDigPos,
382 String truncatedWholePart) {
Hans Boehma0e45f32015-05-30 13:20:35 -0700383 initPositions(initPrec, msd, leastDigPos, truncatedWholePart);
Hans Boehm84614952014-11-25 18:46:17 -0800384 redisplay();
385 }
386
Hans Boehma0e45f32015-05-30 13:20:35 -0700387 /**
Hans Boehm5e802f32015-06-22 17:18:52 -0700388 * Set up scroll bounds (mMinPos, mMaxPos, etc.) and determine whether the result is
389 * scrollable, based on the supplied information about the result.
Hans Boehma0e45f32015-05-30 13:20:35 -0700390 * This is unfortunately complicated because we need to predict whether trailing digits
391 * will eventually be replaced by an exponent.
392 * Just appending the exponent during formatting would be simpler, but would produce
393 * jumpier results during transitions.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700394 * Only called from UI thread.
Hans Boehma0e45f32015-05-30 13:20:35 -0700395 */
Hans Boehm5e802f32015-06-22 17:18:52 -0700396 private void initPositions(int initPrecOffset, int msdIndex, int lsdOffset,
397 String truncatedWholePart) {
Hans Boehma0e45f32015-05-30 13:20:35 -0700398 int maxChars = getMaxChars();
Hans Boehm24c91ed2016-06-30 18:53:44 -0700399 mWholeLen = truncatedWholePart.length();
400 // Allow a tiny amount of slop for associativity/rounding differences in length
401 // calculation. If getPreferredPrec() decided it should fit, we want to make it fit, too.
402 // We reserved one extra pixel, so the extra length is OK.
403 final int nSeparatorChars = (int) Math.ceil(
404 separatorChars(truncatedWholePart, truncatedWholePart.length())
405 - getNoEllipsisCredit() - 0.0001f);
406 mWholePartFits = mWholeLen + nSeparatorChars <= maxChars;
Hans Boehma0e45f32015-05-30 13:20:35 -0700407 mLastPos = INVALID;
Hans Boehm5e802f32015-06-22 17:18:52 -0700408 mLsdOffset = lsdOffset;
Hans Boehm24c91ed2016-06-30 18:53:44 -0700409 mAppendExponent = false;
Hans Boehma0e45f32015-05-30 13:20:35 -0700410 // Prevent scrolling past initial position, which is calculated to show leading digits.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700411 mCurrentPos = mMinPos = (int) Math.round(initPrecOffset * mCharWidth);
Hans Boehm5e802f32015-06-22 17:18:52 -0700412 if (msdIndex == Evaluator.INVALID_MSD) {
Hans Boehma0e45f32015-05-30 13:20:35 -0700413 // Possible zero value
Hans Boehm5e802f32015-06-22 17:18:52 -0700414 if (lsdOffset == Integer.MIN_VALUE) {
Hans Boehma0e45f32015-05-30 13:20:35 -0700415 // Definite zero value.
416 mMaxPos = mMinPos;
Hans Boehm24c91ed2016-06-30 18:53:44 -0700417 mMaxCharOffset = (int) Math.round(mMaxPos/mCharWidth);
Hans Boehma0e45f32015-05-30 13:20:35 -0700418 mScrollable = false;
419 } else {
420 // May be very small nonzero value. Allow user to find out.
Hans Boehm5e802f32015-06-22 17:18:52 -0700421 mMaxPos = mMaxCharOffset = MAX_RIGHT_SCROLL;
Hans Boehm24c91ed2016-06-30 18:53:44 -0700422 mMinPos -= mCharWidth; // Allow for future minus sign.
Hans Boehma0e45f32015-05-30 13:20:35 -0700423 mScrollable = true;
424 }
425 return;
426 }
Hans Boehma0e45f32015-05-30 13:20:35 -0700427 int negative = truncatedWholePart.charAt(0) == '-' ? 1 : 0;
Hans Boehm65a99a42016-02-03 18:16:07 -0800428 if (msdIndex > mWholeLen && msdIndex <= mWholeLen + 3) {
Hans Boehm5e802f32015-06-22 17:18:52 -0700429 // Avoid tiny negative exponent; pretend msdIndex is just to the right of decimal point.
Hans Boehm65a99a42016-02-03 18:16:07 -0800430 msdIndex = mWholeLen - 1;
Hans Boehma0e45f32015-05-30 13:20:35 -0700431 }
Hans Boehm24c91ed2016-06-30 18:53:44 -0700432 // Set to position of leftmost significant digit relative to dec. point. Usually negative.
Hans Boehm65a99a42016-02-03 18:16:07 -0800433 int minCharOffset = msdIndex - mWholeLen;
Hans Boehm5e802f32015-06-22 17:18:52 -0700434 if (minCharOffset > -1 && minCharOffset < MAX_LEADING_ZEROES + 2) {
Hans Boehma0e45f32015-05-30 13:20:35 -0700435 // Small number of leading zeroes, avoid scientific notation.
Hans Boehm5e802f32015-06-22 17:18:52 -0700436 minCharOffset = -1;
Hans Boehma0e45f32015-05-30 13:20:35 -0700437 }
Hans Boehm5e802f32015-06-22 17:18:52 -0700438 if (lsdOffset < MAX_RIGHT_SCROLL) {
439 mMaxCharOffset = lsdOffset;
440 if (mMaxCharOffset < -1 && mMaxCharOffset > -(MAX_TRAILING_ZEROES + 2)) {
441 mMaxCharOffset = -1;
Hans Boehma0e45f32015-05-30 13:20:35 -0700442 }
Hans Boehm5e802f32015-06-22 17:18:52 -0700443 // lsdOffset is positive or negative, never 0.
444 int currentExpLen = 0; // Length of required standard scientific notation exponent.
445 if (mMaxCharOffset < -1) {
446 currentExpLen = expLen(-minCharOffset - 1);
447 } else if (minCharOffset > -1 || mMaxCharOffset >= maxChars) {
Hans Boehm24c91ed2016-06-30 18:53:44 -0700448 // Number is either entirely to the right of decimal point, or decimal point is
449 // not visible when scrolled to the right.
Hans Boehm5e802f32015-06-22 17:18:52 -0700450 currentExpLen = expLen(-minCharOffset);
Hans Boehma0e45f32015-05-30 13:20:35 -0700451 }
Hans Boehm24c91ed2016-06-30 18:53:44 -0700452 // Exponent length does not included added decimal point. But whenever we add a
453 // decimal point, we allow an extra character (SCI_NOTATION_EXTRA).
454 final int separatorLength = mWholePartFits && minCharOffset < -3 ? nSeparatorChars : 0;
455 mScrollable = (mMaxCharOffset + currentExpLen + separatorLength - minCharOffset
456 + negative >= maxChars);
457 // Now adjust mMaxCharOffset for any required exponent.
Hans Boehm5e802f32015-06-22 17:18:52 -0700458 int newMaxCharOffset;
459 if (currentExpLen > 0) {
460 if (mScrollable) {
461 // We'll use exponent corresponding to leastDigPos when scrolled to right.
462 newMaxCharOffset = mMaxCharOffset + expLen(-lsdOffset);
463 } else {
464 newMaxCharOffset = mMaxCharOffset + currentExpLen;
465 }
466 if (mMaxCharOffset <= -1 && newMaxCharOffset > -1) {
467 // Very unlikely; just drop exponent.
468 mMaxCharOffset = -1;
469 } else {
Hans Boehm24c91ed2016-06-30 18:53:44 -0700470 mMaxCharOffset = Math.min(newMaxCharOffset, MAX_RIGHT_SCROLL);
Hans Boehma0e45f32015-05-30 13:20:35 -0700471 }
Hans Boehm24c91ed2016-06-30 18:53:44 -0700472 mMaxPos = Math.min((int) Math.round(mMaxCharOffset * mCharWidth),
473 MAX_RIGHT_SCROLL);
474 } else if (!mWholePartFits && !mScrollable) {
475 // Corner case in which entire number fits, but not with grouping separators. We
476 // will use an exponent in un-scrolled position, which may hide digits. Scrolling
477 // by one character will remove the exponent and reveal the last digits. Note
478 // that in the forced scientific notation case, the exponent length is not
479 // factored into mMaxCharOffset, since we do not want such an increase to impact
480 // scrolling behavior. In the unscrollable case, we thus have to append the
481 // exponent at the end using the forcePrecision argument to formatResult, in order
482 // to ensure that we get the entire result.
483 mScrollable = (mMaxCharOffset + expLen(-minCharOffset - 1) - minCharOffset
484 + negative >= maxChars);
485 if (mScrollable) {
486 mMaxPos = (int) Math.ceil(mMinPos + mCharWidth);
487 // Single character scroll will remove exponent and show remaining piece.
488 } else {
489 mMaxPos = mMinPos;
490 mAppendExponent = true;
491 }
492 } else {
493 mMaxPos = Math.min((int) Math.round(mMaxCharOffset * mCharWidth),
494 MAX_RIGHT_SCROLL);
Hans Boehma0e45f32015-05-30 13:20:35 -0700495 }
Hans Boehma0e45f32015-05-30 13:20:35 -0700496 if (!mScrollable) {
497 // Position the number consistently with our assumptions to make sure it
498 // actually fits.
499 mCurrentPos = mMaxPos;
500 }
501 } else {
Hans Boehm5e802f32015-06-22 17:18:52 -0700502 mMaxPos = mMaxCharOffset = MAX_RIGHT_SCROLL;
Hans Boehma0e45f32015-05-30 13:20:35 -0700503 mScrollable = true;
504 }
505 }
506
Hans Boehm24c91ed2016-06-30 18:53:44 -0700507 /**
508 * Display error message indicated by resourceId.
509 * UI thread only.
510 */
Hans Boehm8f051c32016-10-03 16:53:58 -0700511 @Override
512 public void onError(long index, int resourceId) {
Hans Boehm760a9dc2015-04-20 10:27:12 -0700513 mValid = true;
Christine Franksafe28bb2016-07-29 17:24:52 -0700514 setLongClickable(false);
Hans Boehm84614952014-11-25 18:46:17 -0800515 mScrollable = false;
Hans Boehm14344ff2016-06-08 13:01:51 -0700516 final String msg = getContext().getString(resourceId);
Hans Boehm14344ff2016-06-08 13:01:51 -0700517 final float measuredWidth = Layout.getDesiredWidth(msg, getPaint());
Hans Boehm24c91ed2016-06-30 18:53:44 -0700518 if (measuredWidth > mWidthConstraint) {
Hans Boehm14344ff2016-06-08 13:01:51 -0700519 // Multiply by .99 to avoid rounding effects.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700520 final float scaleFactor = 0.99f * mWidthConstraint / measuredWidth;
Hans Boehm14344ff2016-06-08 13:01:51 -0700521 final RelativeSizeSpan smallTextSpan = new RelativeSizeSpan(scaleFactor);
522 final SpannableString scaledMsg = new SpannableString(msg);
523 scaledMsg.setSpan(smallTextSpan, 0, msg.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
524 setText(scaledMsg);
525 } else {
526 setText(msg);
527 }
Hans Boehm84614952014-11-25 18:46:17 -0800528 }
529
Hans Boehm013969e2015-04-13 20:29:47 -0700530 private final int MAX_COPY_SIZE = 1000000;
531
Hans Boehma0e45f32015-05-30 13:20:35 -0700532 /*
533 * Return the most significant digit position in the given string or Evaluator.INVALID_MSD.
Hans Boehm3666e632015-07-27 18:33:12 -0700534 * Unlike Evaluator.getMsdIndexOf, we treat a final 1 as significant.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700535 * Pure function; callable from anywhere.
Hans Boehma0e45f32015-05-30 13:20:35 -0700536 */
Hans Boehm3666e632015-07-27 18:33:12 -0700537 public static int getNaiveMsdIndexOf(String s) {
Hans Boehm65a99a42016-02-03 18:16:07 -0800538 final int len = s.length();
Hans Boehma0e45f32015-05-30 13:20:35 -0700539 for (int i = 0; i < len; ++i) {
540 char c = s.charAt(i);
541 if (c != '-' && c != '.' && c != '0') {
542 return i;
543 }
544 }
545 return Evaluator.INVALID_MSD;
546 }
547
Hans Boehm24c91ed2016-06-30 18:53:44 -0700548 /**
549 * Format a result returned by Evaluator.getString() into a single line containing ellipses
550 * (if appropriate) and an exponent (if appropriate).
551 * We add two distinct kinds of exponents:
552 * (1) If the final result contains the leading digit we use standard scientific notation.
553 * (2) If not, we add an exponent corresponding to an interpretation of the final result as
554 * an integer.
555 * We add an ellipsis on the left if the result was truncated.
556 * We add ellipses and exponents in a way that leaves most digits in the position they
557 * would have been in had we not done so. This minimizes jumps as a result of scrolling.
558 * Result is NOT internationalized, uses "E" for exponent.
559 * Called only from UI thread; We sometimes omit locking for fields.
560 * @param precOffset The value that was passed to getString. Identifies the significance of
561 the rightmost digit. A value of 1 means the rightmost digits corresponds to tenths.
562 * @param maxDigs The maximum number of characters in the result
563 * @param truncated The in parameter was already truncated, beyond possibly removing the
564 minus sign.
565 * @param negative The in parameter represents a negative result. (Minus sign may be removed
566 without setting truncated.)
567 * @param lastDisplayedOffset If not null, we set lastDisplayedOffset[0] to the offset of
568 the last digit actually appearing in the display.
569 * @param forcePrecision If true, we make sure that the last displayed digit corresponds to
570 precOffset, and allow maxDigs to be exceeded in adding the exponent and commas.
571 * @param forceSciNotation Force scientific notation. May be set because we don't have
572 space for grouping separators, but whole number otherwise fits.
573 * @param insertCommas Insert commas (literally, not internationalized) as digit separators.
574 We only ever do this for the integral part of a number, and only when no
575 exponent is displayed in the initial position. The combination of which means
576 that we only do it when no exponent is displayed.
577 We insert commas in a way that does consider the width of the actual localized digit
578 separator. Commas count towards maxDigs as the appropriate fraction of a digit.
579 */
580 private String formatResult(String in, int precOffset, int maxDigs, boolean truncated,
581 boolean negative, int lastDisplayedOffset[], boolean forcePrecision,
582 boolean forceSciNotation, boolean insertCommas) {
Hans Boehm5e802f32015-06-22 17:18:52 -0700583 final int minusSpace = negative ? 1 : 0;
Hans Boehm3666e632015-07-27 18:33:12 -0700584 final int msdIndex = truncated ? -1 : getNaiveMsdIndexOf(in); // INVALID_MSD is OK.
Hans Boehm5e802f32015-06-22 17:18:52 -0700585 String result = in;
Hans Boehm24c91ed2016-06-30 18:53:44 -0700586 boolean needEllipsis = false;
Hans Boehm73ecff22015-09-03 16:04:50 -0700587 if (truncated || (negative && result.charAt(0) != '-')) {
Hans Boehm24c91ed2016-06-30 18:53:44 -0700588 needEllipsis = true;
Hans Boehm73ecff22015-09-03 16:04:50 -0700589 result = KeyMaps.ELLIPSIS + result.substring(1, result.length());
590 // Ellipsis may be removed again in the type(1) scientific notation case.
591 }
592 final int decIndex = result.indexOf('.');
Hans Boehm65a99a42016-02-03 18:16:07 -0800593 if (lastDisplayedOffset != null) {
594 lastDisplayedOffset[0] = precOffset;
595 }
Hans Boehm24c91ed2016-06-30 18:53:44 -0700596 if (forceSciNotation || (decIndex == -1 || msdIndex != Evaluator.INVALID_MSD
Hans Boehm5e802f32015-06-22 17:18:52 -0700597 && msdIndex - decIndex > MAX_LEADING_ZEROES + 1) && precOffset != -1) {
Hans Boehm24c91ed2016-06-30 18:53:44 -0700598 // Either:
599 // 1) No decimal point displayed, and it's not just to the right of the last digit, or
600 // 2) we are at the front of a number whos integral part is too large to allow
601 // comma insertion, or
602 // 3) we should suppress leading zeroes.
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700603 // Add an exponent to let the user track which digits are currently displayed.
Hans Boehm5e802f32015-06-22 17:18:52 -0700604 // Start with type (2) exponent if we dropped no digits. -1 accounts for decimal point.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700605 // We currently never show digit separators together with an exponent.
Hans Boehm5e802f32015-06-22 17:18:52 -0700606 final int initExponent = precOffset > 0 ? -precOffset : -precOffset - 1;
607 int exponent = initExponent;
Hans Boehm08e8f322015-04-21 13:18:38 -0700608 boolean hasPoint = false;
Hans Boehm5e802f32015-06-22 17:18:52 -0700609 if (!truncated && msdIndex < maxDigs - 1
610 && result.length() - msdIndex + 1 + minusSpace
611 <= maxDigs + SCI_NOTATION_EXTRA) {
612 // Type (1) exponent computation and transformation:
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700613 // Leading digit is in display window. Use standard calculator scientific notation
614 // with one digit to the left of the decimal point. Insert decimal point and
615 // delete leading zeroes.
Hans Boehma0e45f32015-05-30 13:20:35 -0700616 // We try to keep leading digits roughly in position, and never
Hans Boehmf6dae112015-06-18 17:57:50 -0700617 // lengthen the result by more than SCI_NOTATION_EXTRA.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700618 if (decIndex > msdIndex) {
619 // In the forceSciNotation, we can have a decimal point in the relevant digit
620 // range. Remove it.
621 result = result.substring(0, decIndex)
622 + result.substring(decIndex + 1, result.length());
623 // msdIndex and precOffset unaffected.
624 }
Hans Boehm5e802f32015-06-22 17:18:52 -0700625 final int resLen = result.length();
626 String fraction = result.substring(msdIndex + 1, resLen);
627 result = (negative ? "-" : "") + result.substring(msdIndex, msdIndex + 1)
628 + "." + fraction;
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700629 // Original exp was correct for decimal point at right of fraction.
630 // Adjust by length of fraction.
Hans Boehm5e802f32015-06-22 17:18:52 -0700631 exponent = initExponent + resLen - msdIndex - 1;
Hans Boehm08e8f322015-04-21 13:18:38 -0700632 hasPoint = true;
633 }
Hans Boehm73ecff22015-09-03 16:04:50 -0700634 // Exponent can't be zero.
635 // Actually add the exponent of either type:
636 if (!forcePrecision) {
637 int dropDigits; // Digits to drop to make room for exponent.
638 if (hasPoint) {
639 // Type (1) exponent.
640 // Drop digits even if there is room. Otherwise the scrolling gets jumpy.
641 dropDigits = expLen(exponent);
642 if (dropDigits >= result.length() - 1) {
643 // Jumpy is better than no mantissa. Probably impossible anyway.
644 dropDigits = Math.max(result.length() - 2, 0);
Hans Boehma0e45f32015-05-30 13:20:35 -0700645 }
Hans Boehm73ecff22015-09-03 16:04:50 -0700646 } else {
647 // Type (2) exponent.
648 // Exponent depends on the number of digits we drop, which depends on
649 // exponent ...
650 for (dropDigits = 2; expLen(initExponent + dropDigits) > dropDigits;
651 ++dropDigits) {}
652 exponent = initExponent + dropDigits;
653 if (precOffset - dropDigits > mLsdOffset) {
654 // This can happen if e.g. result = 10^40 + 10^10
655 // It turns out we would otherwise display ...10e9 because it takes
656 // the same amount of space as ...1e10 but shows one more digit.
657 // But we don't want to display a trailing zero, even if it's free.
658 ++dropDigits;
659 ++exponent;
660 }
Hans Boehm08e8f322015-04-21 13:18:38 -0700661 }
Hans Boehm73ecff22015-09-03 16:04:50 -0700662 result = result.substring(0, result.length() - dropDigits);
Hans Boehm65a99a42016-02-03 18:16:07 -0800663 if (lastDisplayedOffset != null) {
664 lastDisplayedOffset[0] -= dropDigits;
665 }
Hans Boehm73ecff22015-09-03 16:04:50 -0700666 }
667 result = result + "E" + Integer.toString(exponent);
Hans Boehm24c91ed2016-06-30 18:53:44 -0700668 } else if (insertCommas) {
669 // Add commas to the whole number section, and then truncate on left to fit,
670 // counting commas as a fractional digit.
671 final int wholeStart = needEllipsis ? 1 : 0;
672 int orig_length = result.length();
673 final float nCommaChars;
674 if (decIndex != -1) {
675 nCommaChars = separatorChars(result, decIndex);
676 result = StringUtils.addCommas(result, wholeStart, decIndex)
677 + result.substring(decIndex, orig_length);
678 } else {
679 nCommaChars = separatorChars(result, orig_length);
680 result = StringUtils.addCommas(result, wholeStart, orig_length);
681 }
682 if (needEllipsis) {
683 orig_length -= 1; // Exclude ellipsis.
684 }
685 final float len = orig_length + nCommaChars;
686 int deletedChars = 0;
687 final float ellipsisCredit = getNoEllipsisCredit();
688 final float decimalCredit = getDecimalCredit();
689 final float effectiveLen = len - (decIndex == -1 ? 0 : getDecimalCredit());
690 final float ellipsisAdjustment =
691 needEllipsis ? mNoExponentCredit : getNoEllipsisCredit();
692 // As above, we allow for a tiny amount of extra length here, for consistency with
693 // getPreferredPrec().
694 if (effectiveLen - ellipsisAdjustment > (float) (maxDigs - wholeStart) + 0.0001f
695 && !forcePrecision) {
696 float deletedWidth = 0.0f;
697 while (effectiveLen - mNoExponentCredit - deletedWidth
698 > (float) (maxDigs - 1 /* for ellipsis */)) {
699 if (result.charAt(deletedChars) == ',') {
700 deletedWidth += mGroupingSeparatorWidthRatio;
701 } else {
702 deletedWidth += 1.0f;
703 }
704 deletedChars++;
705 }
706 }
707 if (deletedChars > 0) {
708 result = KeyMaps.ELLIPSIS + result.substring(deletedChars, result.length());
709 } else if (needEllipsis) {
710 result = KeyMaps.ELLIPSIS + result;
711 }
Hans Boehm5e802f32015-06-22 17:18:52 -0700712 }
713 return result;
Hans Boehm08e8f322015-04-21 13:18:38 -0700714 }
715
Hans Boehmf6dae112015-06-18 17:57:50 -0700716 /**
717 * Get formatted, but not internationalized, result from mEvaluator.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700718 * @param precOffset requested position (1 = tenths) of last included digit
719 * @param maxSize maximum number of characters (more or less) in result
720 * @param lastDisplayedOffset zeroth entry is set to actual offset of last included digit,
Hans Boehm65a99a42016-02-03 18:16:07 -0800721 * after adjusting for exponent, etc. May be null.
Hans Boehmf6dae112015-06-18 17:57:50 -0700722 * @param forcePrecision Ensure that last included digit is at pos, at the expense
723 * of treating maxSize as a soft limit.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700724 * @param forceSciNotation Force scientific notation, even if not required by maxSize.
725 * @param insertCommas Insert commas as digit separators.
Hans Boehmf6dae112015-06-18 17:57:50 -0700726 */
Hans Boehm5e802f32015-06-22 17:18:52 -0700727 private String getFormattedResult(int precOffset, int maxSize, int lastDisplayedOffset[],
Hans Boehm24c91ed2016-06-30 18:53:44 -0700728 boolean forcePrecision, boolean forceSciNotation, boolean insertCommas) {
Hans Boehm08e8f322015-04-21 13:18:38 -0700729 final boolean truncated[] = new boolean[1];
730 final boolean negative[] = new boolean[1];
Hans Boehm5e802f32015-06-22 17:18:52 -0700731 final int requestedPrecOffset[] = {precOffset};
Hans Boehm8f051c32016-10-03 16:53:58 -0700732 final String rawResult = mEvaluator.getString(mIndex, requestedPrecOffset, mMaxCharOffset,
733 maxSize, truncated, negative, this);
Hans Boehm5e802f32015-06-22 17:18:52 -0700734 return formatResult(rawResult, requestedPrecOffset[0], maxSize, truncated[0], negative[0],
Hans Boehm24c91ed2016-06-30 18:53:44 -0700735 lastDisplayedOffset, forcePrecision, forceSciNotation, insertCommas);
Hans Boehm08e8f322015-04-21 13:18:38 -0700736 }
737
Hans Boehm65a99a42016-02-03 18:16:07 -0800738 /**
739 * Return entire result (within reason) up to current displayed precision.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700740 * @param withSeparators Add digit separators
Hans Boehm65a99a42016-02-03 18:16:07 -0800741 */
Hans Boehm24c91ed2016-06-30 18:53:44 -0700742 public String getFullText(boolean withSeparators) {
Hans Boehm760a9dc2015-04-20 10:27:12 -0700743 if (!mValid) return "";
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700744 if (!mScrollable) return getText().toString();
Hans Boehm5e802f32015-06-22 17:18:52 -0700745 return KeyMaps.translateResult(getFormattedResult(mLastDisplayedOffset, MAX_COPY_SIZE,
Hans Boehm24c91ed2016-06-30 18:53:44 -0700746 null, true /* forcePrecision */, false /* forceSciNotation */, withSeparators));
Hans Boehm84614952014-11-25 18:46:17 -0800747 }
748
Hans Boehm24c91ed2016-06-30 18:53:44 -0700749 /**
750 * Did the above produce a correct result?
751 * UI thread only.
752 */
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700753 public boolean fullTextIsExact() {
Hans Boehm24c91ed2016-06-30 18:53:44 -0700754 return !mScrollable || (mMaxCharOffset == getCharOffset(mCurrentPos)
755 && mMaxCharOffset != MAX_RIGHT_SCROLL);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700756 }
757
Hans Boehm61568a12015-05-18 18:25:41 -0700758 /**
Hans Boehm65a99a42016-02-03 18:16:07 -0800759 * Get entire result up to current displayed precision, or up to MAX_COPY_EXTRA additional
760 * digits, if it will lead to an exact result.
761 */
762 public String getFullCopyText() {
763 if (!mValid
764 || mLsdOffset == Integer.MAX_VALUE
765 || fullTextIsExact()
766 || mWholeLen > MAX_RECOMPUTE_DIGITS
767 || mWholeLen + mLsdOffset > MAX_RECOMPUTE_DIGITS
768 || mLsdOffset - mLastDisplayedOffset > MAX_COPY_EXTRA) {
Hans Boehm24c91ed2016-06-30 18:53:44 -0700769 return getFullText(false /* withSeparators */);
Hans Boehm65a99a42016-02-03 18:16:07 -0800770 }
771 // It's reasonable to compute and copy the exact result instead.
772 final int nonNegLsdOffset = Math.max(0, mLsdOffset);
Hans Boehm8f051c32016-10-03 16:53:58 -0700773 final String rawResult = mEvaluator.getResult(mIndex).toStringTruncated(nonNegLsdOffset);
Hans Boehm65a99a42016-02-03 18:16:07 -0800774 final String formattedResult = formatResult(rawResult, nonNegLsdOffset, MAX_COPY_SIZE,
Hans Boehm24c91ed2016-06-30 18:53:44 -0700775 false, rawResult.charAt(0) == '-', null, true /* forcePrecision */,
776 false /* forceSciNotation */, false /* insertCommas */);
Hans Boehm65a99a42016-02-03 18:16:07 -0800777 return KeyMaps.translateResult(formattedResult);
778 }
779
780 /**
Hans Boehm61568a12015-05-18 18:25:41 -0700781 * Return the maximum number of characters that will fit in the result display.
Hans Boehm8f051c32016-10-03 16:53:58 -0700782 * May be called asynchronously from non-UI thread. From Evaluator.CharMetricsInfo.
Hans Boehm61568a12015-05-18 18:25:41 -0700783 */
Hans Boehm8f051c32016-10-03 16:53:58 -0700784 @Override
785 public int getMaxChars() {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700786 int result;
787 synchronized(mWidthLock) {
Justin Klaassen44595162015-05-28 17:55:20 -0700788 result = (int) Math.floor(mWidthConstraint / mCharWidth);
Christine Franks7452d3a2016-10-27 13:41:18 -0700789 // We can apparently finish evaluating before onMeasure in CalculatorFormula has been
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700790 // called, in which case we get 0 or -1 as the width constraint.
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700791 }
Hans Boehm84614952014-11-25 18:46:17 -0800792 if (result <= 0) {
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700793 // Return something conservatively big, to force sufficient evaluation.
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700794 return MAX_WIDTH;
Hans Boehm84614952014-11-25 18:46:17 -0800795 } else {
Hans Boehm80018c82015-08-02 16:59:07 -0700796 return result;
Hans Boehm84614952014-11-25 18:46:17 -0800797 }
798 }
799
Hans Boehm61568a12015-05-18 18:25:41 -0700800 /**
Justin Klaassen44595162015-05-28 17:55:20 -0700801 * @return {@code true} if the currently displayed result is scrollable
Hans Boehm61568a12015-05-18 18:25:41 -0700802 */
Justin Klaassen44595162015-05-28 17:55:20 -0700803 public boolean isScrollable() {
804 return mScrollable;
Hans Boehm61568a12015-05-18 18:25:41 -0700805 }
806
Hans Boehm24c91ed2016-06-30 18:53:44 -0700807 /**
808 * Map pixel position to digit offset.
809 * UI thread only.
810 */
811 int getCharOffset(int pos) {
812 return (int) Math.round(pos / mCharWidth); // Lock not needed.
Hans Boehm013969e2015-04-13 20:29:47 -0700813 }
814
Hans Boehm84614952014-11-25 18:46:17 -0800815 void clear() {
Hans Boehm760a9dc2015-04-20 10:27:12 -0700816 mValid = false;
Hans Boehm1176f232015-05-11 16:26:03 -0700817 mScrollable = false;
Hans Boehm84614952014-11-25 18:46:17 -0800818 setText("");
Christine Franksafe28bb2016-07-29 17:24:52 -0700819 setLongClickable(false);
Hans Boehm84614952014-11-25 18:46:17 -0800820 }
821
Hans Boehm8f051c32016-10-03 16:53:58 -0700822 @Override
823 public void onCancelled(long index) {
824 clear();
825 }
826
Hans Boehm24c91ed2016-06-30 18:53:44 -0700827 /**
828 * Refresh display.
Hans Boehm8f051c32016-10-03 16:53:58 -0700829 * Only called in UI thread. Index argument is currently ignored.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700830 */
Hans Boehm8f051c32016-10-03 16:53:58 -0700831 @Override
832 public void onReevaluate(long index) {
833 redisplay();
834 }
835
836 public void redisplay() {
Christine Franks6f6c24a2016-09-08 18:21:47 -0700837 if (mScroller.isFinished() && length() > 0) {
Christine Franksd21205c2016-08-04 10:06:15 -0700838 setAccessibilityLiveRegion(ACCESSIBILITY_LIVE_REGION_POLITE);
839 }
Hans Boehm24c91ed2016-06-30 18:53:44 -0700840 int currentCharOffset = getCharOffset(mCurrentPos);
Hans Boehm84614952014-11-25 18:46:17 -0800841 int maxChars = getMaxChars();
Hans Boehm5e802f32015-06-22 17:18:52 -0700842 int lastDisplayedOffset[] = new int[1];
Hans Boehm24c91ed2016-06-30 18:53:44 -0700843 String result = getFormattedResult(currentCharOffset, maxChars, lastDisplayedOffset,
844 mAppendExponent /* forcePrecision; preserve entire result */,
845 !mWholePartFits
846 && currentCharOffset == getCharOffset(mMinPos) /* forceSciNotation */,
847 mWholePartFits /* insertCommas */ );
Hans Boehm0b9806f2015-06-29 16:07:15 -0700848 int expIndex = result.indexOf('E');
Hans Boehm013969e2015-04-13 20:29:47 -0700849 result = KeyMaps.translateResult(result);
Hans Boehm5e802f32015-06-22 17:18:52 -0700850 if (expIndex > 0 && result.indexOf('.') == -1) {
Hans Boehm84614952014-11-25 18:46:17 -0800851 // Gray out exponent if used as position indicator
852 SpannableString formattedResult = new SpannableString(result);
Hans Boehm5e802f32015-06-22 17:18:52 -0700853 formattedResult.setSpan(mExponentColorSpan, expIndex, result.length(),
Hans Boehm84614952014-11-25 18:46:17 -0800854 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
855 setText(formattedResult);
856 } else {
857 setText(result);
858 }
Hans Boehm5e802f32015-06-22 17:18:52 -0700859 mLastDisplayedOffset = lastDisplayedOffset[0];
Hans Boehm760a9dc2015-04-20 10:27:12 -0700860 mValid = true;
Christine Franksafe28bb2016-07-29 17:24:52 -0700861 setLongClickable(true);
Hans Boehm84614952014-11-25 18:46:17 -0800862 }
863
864 @Override
Christine Franks6f6c24a2016-09-08 18:21:47 -0700865 protected void onTextChanged(java.lang.CharSequence text, int start, int lengthBefore,
866 int lengthAfter) {
867 super.onTextChanged(text, start, lengthBefore, lengthAfter);
868
869 if (!mScrollable || mScroller.isFinished()) {
870 if (lengthBefore == 0 && lengthAfter > 0) {
871 setAccessibilityLiveRegion(ACCESSIBILITY_LIVE_REGION_POLITE);
872 setContentDescription(null);
873 } else if (lengthBefore > 0 && lengthAfter == 0) {
874 setAccessibilityLiveRegion(ACCESSIBILITY_LIVE_REGION_NONE);
875 setContentDescription(getContext().getString(R.string.desc_result));
876 }
877 }
878 }
879
880 @Override
Hans Boehm84614952014-11-25 18:46:17 -0800881 public void computeScroll() {
Christine Franks6f6c24a2016-09-08 18:21:47 -0700882 if (!mScrollable) {
883 return;
884 }
885
Hans Boehm84614952014-11-25 18:46:17 -0800886 if (mScroller.computeScrollOffset()) {
887 mCurrentPos = mScroller.getCurrX();
Hans Boehm24c91ed2016-06-30 18:53:44 -0700888 if (getCharOffset(mCurrentPos) != getCharOffset(mLastPos)) {
Hans Boehm84614952014-11-25 18:46:17 -0800889 mLastPos = mCurrentPos;
890 redisplay();
891 }
Christine Franks6f6c24a2016-09-08 18:21:47 -0700892 }
893
894 if (!mScroller.isFinished()) {
Justin Klaassen44595162015-05-28 17:55:20 -0700895 postInvalidateOnAnimation();
Christine Franksd21205c2016-08-04 10:06:15 -0700896 setAccessibilityLiveRegion(ACCESSIBILITY_LIVE_REGION_NONE);
Christine Franks6f6c24a2016-09-08 18:21:47 -0700897 } else if (length() > 0){
Christine Franksd21205c2016-08-04 10:06:15 -0700898 setAccessibilityLiveRegion(ACCESSIBILITY_LIVE_REGION_POLITE);
Hans Boehm84614952014-11-25 18:46:17 -0800899 }
900 }
901
Chenjie Yu3937b652016-06-01 23:14:26 -0700902 /**
903 * Use ActionMode for copy support on M and higher.
904 */
905 @TargetApi(Build.VERSION_CODES.M)
906 private void setupActionMode() {
907 mCopyActionModeCallback = new ActionMode.Callback2() {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700908
Chenjie Yu3937b652016-06-01 23:14:26 -0700909 @Override
910 public boolean onCreateActionMode(ActionMode mode, Menu menu) {
911 final MenuInflater inflater = mode.getMenuInflater();
912 return createCopyMenu(inflater, menu);
913 }
Hans Boehm7f83e362015-06-10 15:41:04 -0700914
Chenjie Yu3937b652016-06-01 23:14:26 -0700915 @Override
916 public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
917 return false; // Return false if nothing is done
918 }
Hans Boehm7f83e362015-06-10 15:41:04 -0700919
Chenjie Yu3937b652016-06-01 23:14:26 -0700920 @Override
921 public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
922 if (onMenuItemClick(item)) {
Hans Boehm65a99a42016-02-03 18:16:07 -0800923 mode.finish();
924 return true;
Chenjie Yu3937b652016-06-01 23:14:26 -0700925 } else {
926 return false;
Hans Boehm65a99a42016-02-03 18:16:07 -0800927 }
Chenjie Yu3937b652016-06-01 23:14:26 -0700928 }
929
930 @Override
931 public void onDestroyActionMode(ActionMode mode) {
932 unhighlightResult();
933 mActionMode = null;
934 }
935
936 @Override
937 public void onGetContentRect(ActionMode mode, View view, Rect outRect) {
938 super.onGetContentRect(mode, view, outRect);
939
940 outRect.left += view.getPaddingLeft();
941 outRect.top += view.getPaddingTop();
942 outRect.right -= view.getPaddingRight();
943 outRect.bottom -= view.getPaddingBottom();
944 final int width = (int) Layout.getDesiredWidth(getText(), getPaint());
945 if (width < outRect.width()) {
946 outRect.left = outRect.right - width;
947 }
948
949 if (!BuildCompat.isAtLeastN()) {
950 // The CAB (prior to N) only takes the translation of a view into account, so
951 // if a scale is applied to the view then the offset outRect will end up being
952 // positioned incorrectly. We workaround that limitation by manually applying
953 // the scale to the outRect, which the CAB will then offset to the correct
954 // position.
955 final float scaleX = view.getScaleX();
956 final float scaleY = view.getScaleY();
957 outRect.left *= scaleX;
958 outRect.right *= scaleX;
959 outRect.top *= scaleY;
960 outRect.bottom *= scaleY;
961 }
962 }
963 };
964 setOnLongClickListener(new View.OnLongClickListener() {
965 @Override
966 public boolean onLongClick(View v) {
967 if (mValid) {
968 mActionMode = startActionMode(mCopyActionModeCallback,
969 ActionMode.TYPE_FLOATING);
970 return true;
971 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700972 return false;
973 }
Chenjie Yu3937b652016-06-01 23:14:26 -0700974 });
975 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700976
Chenjie Yu3937b652016-06-01 23:14:26 -0700977 /**
978 * Use ContextMenu for copy support on L and lower.
979 */
980 private void setupContextMenu() {
981 setOnCreateContextMenuListener(new OnCreateContextMenuListener() {
982 @Override
983 public void onCreateContextMenu(ContextMenu contextMenu, View view,
984 ContextMenu.ContextMenuInfo contextMenuInfo) {
985 final MenuInflater inflater = new MenuInflater(getContext());
986 createCopyMenu(inflater, contextMenu);
987 mContextMenu = contextMenu;
988 for(int i = 0; i < contextMenu.size(); i ++) {
989 contextMenu.getItem(i).setOnMenuItemClickListener(CalculatorResult.this);
990 }
Hans Boehm7f83e362015-06-10 15:41:04 -0700991 }
Chenjie Yu3937b652016-06-01 23:14:26 -0700992 });
993 setOnLongClickListener(new View.OnLongClickListener() {
994 @Override
995 public boolean onLongClick(View v) {
996 if (mValid) {
997 return showContextMenu();
998 }
999 return false;
Justin Klaassenf1b61f42016-04-27 16:00:11 -07001000 }
Chenjie Yu3937b652016-06-01 23:14:26 -07001001 });
1002 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001003
Chenjie Yu3937b652016-06-01 23:14:26 -07001004 private boolean createCopyMenu(MenuInflater inflater, Menu menu) {
1005 inflater.inflate(R.menu.copy, menu);
1006 highlightResult();
1007 return true;
1008 }
1009
1010 public boolean stopActionModeOrContextMenu() {
Hans Boehm1176f232015-05-11 16:26:03 -07001011 if (mActionMode != null) {
1012 mActionMode.finish();
1013 return true;
1014 }
Chenjie Yu3937b652016-06-01 23:14:26 -07001015 if (mContextMenu != null) {
1016 unhighlightResult();
1017 mContextMenu.close();
1018 return true;
1019 }
Hans Boehm1176f232015-05-11 16:26:03 -07001020 return false;
1021 }
1022
Chenjie Yu3937b652016-06-01 23:14:26 -07001023 private void highlightResult() {
1024 final Spannable text = (Spannable) getText();
1025 text.setSpan(mHighlightSpan, 0, text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
1026 }
1027
1028 private void unhighlightResult() {
1029 final Spannable text = (Spannable) getText();
1030 text.removeSpan(mHighlightSpan);
1031 }
1032
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001033 private void setPrimaryClip(ClipData clip) {
1034 ClipboardManager clipboard = (ClipboardManager) getContext().
Hans Boehmc01cd7f2015-05-12 18:32:19 -07001035 getSystemService(Context.CLIPBOARD_SERVICE);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001036 clipboard.setPrimaryClip(clip);
1037 }
1038
1039 private void copyContent() {
Hans Boehm65a99a42016-02-03 18:16:07 -08001040 final CharSequence text = getFullCopyText();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001041 ClipboardManager clipboard =
Hans Boehmc01cd7f2015-05-12 18:32:19 -07001042 (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
1043 // We include a tag URI, to allow us to recognize our own results and handle them
1044 // specially.
Hans Boehm8f051c32016-10-03 16:53:58 -07001045 ClipData.Item newItem = new ClipData.Item(text, null, mEvaluator.capture(mIndex));
Hans Boehmc01cd7f2015-05-12 18:32:19 -07001046 String[] mimeTypes = new String[] {ClipDescription.MIMETYPE_TEXT_PLAIN};
1047 ClipData cd = new ClipData("calculator result", mimeTypes, newItem);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001048 clipboard.setPrimaryClip(cd);
Hans Boehmc01cd7f2015-05-12 18:32:19 -07001049 Toast.makeText(getContext(), R.string.text_copied_toast, Toast.LENGTH_SHORT).show();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001050 }
1051
Chenjie Yu3937b652016-06-01 23:14:26 -07001052 @Override
1053 public boolean onMenuItemClick(MenuItem item) {
1054 switch (item.getItemId()) {
1055 case R.id.menu_copy:
Hans Boehm8f051c32016-10-03 16:53:58 -07001056 if (mEvaluator.evaluationInProgress(mIndex)) {
Chenjie Yu3937b652016-06-01 23:14:26 -07001057 // Refuse to copy placeholder characters.
1058 return false;
1059 } else {
1060 copyContent();
1061 unhighlightResult();
1062 return true;
1063 }
1064 default:
1065 return false;
1066 }
1067 }
Hans Boehm84614952014-11-25 18:46:17 -08001068}