blob: 234f602618948943e5f4d002d3c64f1e0304c59e [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.
Chenjie Yu3937b652016-06-01 23:14:26 -070050public class CalculatorResult extends AlignedTextView implements MenuItem.OnMenuItemClickListener {
Hans Boehm61568a12015-05-18 18:25:41 -070051 static final int MAX_RIGHT_SCROLL = 10000000;
Hans Boehm08e8f322015-04-21 13:18:38 -070052 static final int INVALID = MAX_RIGHT_SCROLL + 10000;
Hans Boehm84614952014-11-25 18:46:17 -080053 // A larger value is unlikely to avoid running out of space
54 final OverScroller mScroller;
55 final GestureDetector mGestureDetector;
Hans Boehm84614952014-11-25 18:46:17 -080056 private Evaluator mEvaluator;
57 private boolean mScrollable = false;
58 // A scrollable result is currently displayed.
Hans Boehm760a9dc2015-04-20 10:27:12 -070059 private boolean mValid = false;
Hans Boehmc01cd7f2015-05-12 18:32:19 -070060 // The result holds something valid; either a a number or an error
61 // message.
Hans Boehm5e802f32015-06-22 17:18:52 -070062 // A suffix of "Pos" denotes a pixel offset. Zero represents a scroll position
63 // in which the decimal point is just barely visible on the right of the display.
Hans Boehmc01cd7f2015-05-12 18:32:19 -070064 private int mCurrentPos;// Position of right of display relative to decimal point, in pixels.
65 // Large positive values mean the decimal point is scrolled off the
66 // left of the display. Zero means decimal point is barely displayed
67 // on the right.
Hans Boehm61568a12015-05-18 18:25:41 -070068 private int mLastPos; // Position already reflected in display. Pixels.
Hans Boehm65a99a42016-02-03 18:16:07 -080069 private int mMinPos; // Minimum position to avoid unnecessary blanks on the left. Pixels.
Hans Boehm61568a12015-05-18 18:25:41 -070070 private int mMaxPos; // Maximum position before we start displaying the infinite
71 // sequence of trailing zeroes on the right. Pixels.
Hans Boehm65a99a42016-02-03 18:16:07 -080072 private int mWholeLen; // Length of the whole part of current result.
Hans Boehm5e802f32015-06-22 17:18:52 -070073 // In the following, we use a suffix of Offset to denote a character position in a numeric
74 // string relative to the decimal point. Positive is to the right and negative is to
75 // the left. 1 = tenths position, -1 = units. Integer.MAX_VALUE is sometimes used
76 // for the offset of the last digit in an a nonterminating decimal expansion.
77 // We use the suffix "Index" to denote a zero-based index into a string representing a
78 // result.
Hans Boehm5e802f32015-06-22 17:18:52 -070079 private int mMaxCharOffset; // Character offset from decimal point of rightmost digit
Hans Boehm24c91ed2016-06-30 18:53:44 -070080 // that should be displayed, plus the length of any exponent
81 // needed to display that digit.
82 // Limited to MAX_RIGHT_SCROLL. Often the same as:
Hans Boehm5e802f32015-06-22 17:18:52 -070083 private int mLsdOffset; // Position of least-significant digit in result
84 private int mLastDisplayedOffset; // Offset of last digit actually displayed after adding
Hans Boehmf6dae112015-06-18 17:57:50 -070085 // exponent.
Hans Boehm24c91ed2016-06-30 18:53:44 -070086 private boolean mWholePartFits; // Scientific notation not needed for initial display.
87 private float mNoExponentCredit;
88 // Fraction of digit width saved by avoiding scientific notation.
89 // Only accessed from UI thread.
90 private boolean mAppendExponent;
91 // The result fits entirely in the display, even with an exponent,
92 // but not with grouping separators. Since the result is not
93 // scrollable, and we do not add the exponent to max. scroll position,
94 // append an exponent insteadd of replacing trailing digits.
Justin Klaassen44595162015-05-28 17:55:20 -070095 private final Object mWidthLock = new Object();
Hans Boehm24c91ed2016-06-30 18:53:44 -070096 // Protects the next five fields. These fields are only
97 // Updated by the UI thread, and read accesses by the UI thread
98 // sometimes do not acquire the lock.
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070099 private int mWidthConstraint = -1;
Hans Boehma0e45f32015-05-30 13:20:35 -0700100 // Our total width in pixels minus space for ellipsis.
Justin Klaassen44595162015-05-28 17:55:20 -0700101 private float mCharWidth = 1;
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700102 // Maximum character width. For now we pretend that all characters
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700103 // have this width.
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700104 // TODO: We're not really using a fixed width font. But it appears
105 // to be close enough for the characters we use that the difference
106 // is not noticeable.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700107 private float mGroupingSeparatorWidthRatio;
108 // Fraction of digit width occupied by a digit separator.
109 private float mDecimalCredit;
110 // Fraction of digit width saved by replacing digit with decimal point.
111 private float mNoEllipsisCredit;
112 // Fraction of digit width saved by both replacing ellipsis with digit
113 // and avoiding scientific notation.
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700114 private static final int MAX_WIDTH = 100;
Hans Boehm24c91ed2016-06-30 18:53:44 -0700115 // Maximum number of digits displayed.
Hans Boehm50ed3202015-06-09 14:35:49 -0700116 public static final int MAX_LEADING_ZEROES = 6;
Hans Boehma0e45f32015-05-30 13:20:35 -0700117 // Maximum number of leading zeroes after decimal point before we
118 // switch to scientific notation with negative exponent.
Hans Boehm50ed3202015-06-09 14:35:49 -0700119 public static final int MAX_TRAILING_ZEROES = 6;
Hans Boehma0e45f32015-05-30 13:20:35 -0700120 // Maximum number of trailing zeroes before the decimal point before
121 // we switch to scientific notation with positive exponent.
122 private static final int SCI_NOTATION_EXTRA = 1;
123 // Extra digits for standard scientific notation. In this case we
Hans Boehm80018c82015-08-02 16:59:07 -0700124 // have a decimal point and no ellipsis.
125 // We assume that we do not drop digits to make room for the decimal
126 // point in ordinary scientific notation. Thus >= 1.
Hans Boehm65a99a42016-02-03 18:16:07 -0800127 private static final int MAX_COPY_EXTRA = 100;
128 // The number of extra digits we are willing to compute to copy
129 // a result as an exact number.
130 private static final int MAX_RECOMPUTE_DIGITS = 2000;
131 // The maximum number of digits we're willing to recompute in the UI
132 // thread. We only do this for known rational results, where we
133 // can bound the computation cost.
Chenjie Yu3937b652016-06-01 23:14:26 -0700134 private final ForegroundColorSpan mExponentColorSpan;
135 private final BackgroundColorSpan mHighlightSpan;
Hans Boehm65a99a42016-02-03 18:16:07 -0800136
Hans Boehm1176f232015-05-11 16:26:03 -0700137 private ActionMode mActionMode;
Chenjie Yu3937b652016-06-01 23:14:26 -0700138 private ActionMode.Callback mCopyActionModeCallback;
139 private ContextMenu mContextMenu;
Hans Boehm84614952014-11-25 18:46:17 -0800140
141 public CalculatorResult(Context context, AttributeSet attrs) {
142 super(context, attrs);
143 mScroller = new OverScroller(context);
Chenjie Yu3937b652016-06-01 23:14:26 -0700144 mHighlightSpan = new BackgroundColorSpan(getHighlightColor());
145 mExponentColorSpan = new ForegroundColorSpan(
146 ContextCompat.getColor(context, R.color.display_result_exponent_text_color));
Hans Boehm84614952014-11-25 18:46:17 -0800147 mGestureDetector = new GestureDetector(context,
148 new GestureDetector.SimpleOnGestureListener() {
149 @Override
Justin Klaassend48b7562015-04-16 16:51:38 -0700150 public boolean onDown(MotionEvent e) {
151 return true;
152 }
153 @Override
Hans Boehm84614952014-11-25 18:46:17 -0800154 public boolean onFling(MotionEvent e1, MotionEvent e2,
155 float velocityX, float velocityY) {
156 if (!mScroller.isFinished()) {
157 mCurrentPos = mScroller.getFinalX();
158 }
159 mScroller.forceFinished(true);
Chenjie Yu3937b652016-06-01 23:14:26 -0700160 stopActionModeOrContextMenu();
Hans Boehmfbcef702015-04-27 18:07:47 -0700161 CalculatorResult.this.cancelLongPress();
162 // Ignore scrolls of error string, etc.
163 if (!mScrollable) return true;
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700164 mScroller.fling(mCurrentPos, 0, - (int) velocityX, 0 /* horizontal only */,
Hans Boehm61568a12015-05-18 18:25:41 -0700165 mMinPos, mMaxPos, 0, 0);
Justin Klaassen44595162015-05-28 17:55:20 -0700166 postInvalidateOnAnimation();
Hans Boehm84614952014-11-25 18:46:17 -0800167 return true;
168 }
169 @Override
170 public boolean onScroll(MotionEvent e1, MotionEvent e2,
171 float distanceX, float distanceY) {
Hans Boehm61568a12015-05-18 18:25:41 -0700172 int distance = (int)distanceX;
Hans Boehm84614952014-11-25 18:46:17 -0800173 if (!mScroller.isFinished()) {
174 mCurrentPos = mScroller.getFinalX();
175 }
176 mScroller.forceFinished(true);
Chenjie Yu3937b652016-06-01 23:14:26 -0700177 stopActionModeOrContextMenu();
Hans Boehm84614952014-11-25 18:46:17 -0800178 CalculatorResult.this.cancelLongPress();
179 if (!mScrollable) return true;
Hans Boehm61568a12015-05-18 18:25:41 -0700180 if (mCurrentPos + distance < mMinPos) {
181 distance = mMinPos - mCurrentPos;
182 } else if (mCurrentPos + distance > mMaxPos) {
183 distance = mMaxPos - mCurrentPos;
184 }
Hans Boehm84614952014-11-25 18:46:17 -0800185 int duration = (int)(e2.getEventTime() - e1.getEventTime());
186 if (duration < 1 || duration > 100) duration = 10;
Hans Boehm61568a12015-05-18 18:25:41 -0700187 mScroller.startScroll(mCurrentPos, 0, distance, 0, (int)duration);
Justin Klaassen44595162015-05-28 17:55:20 -0700188 postInvalidateOnAnimation();
Hans Boehm84614952014-11-25 18:46:17 -0800189 return true;
190 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700191 @Override
192 public void onLongPress(MotionEvent e) {
Hans Boehm1176f232015-05-11 16:26:03 -0700193 if (mValid) {
Justin Klaassen3a05c7e2016-03-04 12:40:02 -0800194 performLongClick();
Hans Boehm1176f232015-05-11 16:26:03 -0700195 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700196 }
Hans Boehm84614952014-11-25 18:46:17 -0800197 });
Justin Klaassen3a05c7e2016-03-04 12:40:02 -0800198 setOnTouchListener(new View.OnTouchListener() {
199 @Override
200 public boolean onTouch(View v, MotionEvent event) {
201 return mGestureDetector.onTouchEvent(event);
202 }
203 });
Hans Boehm14344ff2016-06-08 13:01:51 -0700204
Chenjie Yu3937b652016-06-01 23:14:26 -0700205 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
206 setupActionMode();
207 } else {
208 setupContextMenu();
209 }
Hans Boehm14344ff2016-06-08 13:01:51 -0700210
Hans Boehm84614952014-11-25 18:46:17 -0800211 setCursorVisible(false);
Hans Boehm84614952014-11-25 18:46:17 -0800212 }
213
214 void setEvaluator(Evaluator evaluator) {
215 mEvaluator = evaluator;
216 }
217
Hans Boehmcd72f7e2016-06-01 16:21:25 -0700218 // Compute maximum digit width the hard way.
219 private static float getMaxDigitWidth(TextPaint paint) {
220 // Compute the maximum advance width for each digit, thus accounting for between-character
221 // spaces. If we ever support other kinds of digits, we may have to avoid kerning effects
222 // that could reduce the advance width within this particular string.
223 final String allDigits = "0123456789";
224 final float[] widths = new float[allDigits.length()];
225 paint.getTextWidths(allDigits, widths);
226 float maxWidth = 0;
227 for (float x : widths) {
228 maxWidth = Math.max(x, maxWidth);
229 }
230 return maxWidth;
231 }
232
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700233 @Override
234 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Justin Klaassend06f51d2016-08-03 00:41:31 -0700235 if (!isLaidOut()) {
236 // Set a minimum height so scaled error messages won't affect our layout.
237 setMinimumHeight(getLineHeight() + getCompoundPaddingBottom()
238 + getCompoundPaddingTop());
239 }
240
Justin Klaassen44595162015-05-28 17:55:20 -0700241 final TextPaint paint = getPaint();
Hans Boehm80018c82015-08-02 16:59:07 -0700242 final Context context = getContext();
Hans Boehmcd72f7e2016-06-01 16:21:25 -0700243 final float newCharWidth = getMaxDigitWidth(paint);
Hans Boehm80018c82015-08-02 16:59:07 -0700244 // Digits are presumed to have no more than newCharWidth.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700245 // There are two instances when we know that the result is otherwise narrower than
246 // expected:
247 // 1. For standard scientific notation (our type 1), we know that we have a norrow decimal
248 // point and no (usually wide) ellipsis symbol. We allow one extra digit
249 // (SCI_NOTATION_EXTRA) to compensate, and consider that in determining available width.
250 // 2. If we are using digit grouping separators and a decimal point, we give ourselves
251 // a fractional extra space for those separators, the value of which depends on whether
252 // there is also an ellipsis.
253 //
254 // Maximum extra space we need in various cases:
255 // Type 1 scientific notation, assuming ellipsis, minus sign and E are wider than a digit:
256 // Two minus signs + "E" + "." - 3 digits.
257 // Type 2 scientific notation:
258 // Ellipsis + "E" + "-" - 3 digits.
259 // In the absence of scientific notation, we may need a little less space.
260 // We give ourselves a bit of extra credit towards comma insertion and give
261 // ourselves more if we have either
262 // No ellipsis, or
263 // A decimal separator.
264
265 // Calculate extra space we need to reserve, in addition to character count.
Hans Boehm80018c82015-08-02 16:59:07 -0700266 final float decimalSeparatorWidth = Layout.getDesiredWidth(
267 context.getString(R.string.dec_point), paint);
Hans Boehm24c91ed2016-06-30 18:53:44 -0700268 final float minusWidth = Layout.getDesiredWidth(context.getString(R.string.op_sub), paint);
269 final float minusExtraWidth = Math.max(minusWidth - newCharWidth, 0.0f);
270 final float ellipsisWidth = Layout.getDesiredWidth(KeyMaps.ELLIPSIS, paint);
271 final float ellipsisExtraWidth = Math.max(ellipsisWidth - newCharWidth, 0.0f);
272 final float expWidth = Layout.getDesiredWidth(KeyMaps.translateResult("e"), paint);
273 final float expExtraWidth = Math.max(expWidth - newCharWidth, 0.0f);
274 final float type1Extra = 2 * minusExtraWidth + expExtraWidth + decimalSeparatorWidth;
275 final float type2Extra = ellipsisExtraWidth + expExtraWidth + minusExtraWidth;
276 final float extraWidth = Math.max(type1Extra, type2Extra);
277 final int intExtraWidth = (int) Math.ceil(extraWidth) + 1 /* to cover rounding sins */;
Hans Boehm80018c82015-08-02 16:59:07 -0700278 final int newWidthConstraint = MeasureSpec.getSize(widthMeasureSpec)
Hans Boehm24c91ed2016-06-30 18:53:44 -0700279 - (getPaddingLeft() + getPaddingRight()) - intExtraWidth;
280
281 // Calculate other width constants we need to handle grouping separators.
282 final float groupingSeparatorW =
283 Layout.getDesiredWidth(KeyMaps.translateResult(","), paint);
284 // Credits in the absence of any scientific notation:
285 float noExponentCredit = extraWidth - Math.max(ellipsisExtraWidth, minusExtraWidth);
286 final float noEllipsisCredit = extraWidth - minusExtraWidth; // includes noExponentCredit.
287 final float decimalCredit = Math.max(newCharWidth - decimalSeparatorWidth, 0.0f);
288
289 mNoExponentCredit = noExponentCredit / newCharWidth;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700290 synchronized(mWidthLock) {
Hans Boehm013969e2015-04-13 20:29:47 -0700291 mWidthConstraint = newWidthConstraint;
292 mCharWidth = newCharWidth;
Hans Boehm24c91ed2016-06-30 18:53:44 -0700293 mNoEllipsisCredit = noEllipsisCredit / newCharWidth;
294 mDecimalCredit = decimalCredit / newCharWidth;
295 mGroupingSeparatorWidthRatio = groupingSeparatorW / newCharWidth;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700296 }
Hans Boehm14344ff2016-06-08 13:01:51 -0700297
298 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700299 }
300
Hans Boehm24c91ed2016-06-30 18:53:44 -0700301 /**
302 * Return the number of additional digit widths required to add digit separators to
303 * the supplied string prefix.
304 * The string prefix is assumed to represent a whole number, after skipping leading non-digits.
305 * Callable from non-UI thread.
306 */
307 public float separatorChars(String s, int len) {
308 int start = 0;
309 while (start < len && !Character.isDigit(s.charAt(start))) {
310 ++start;
311 }
312 // We assume the rest consists of digits, and for consistency with the rest
313 // of the code, we assume all digits have width mCharWidth.
314 final int nDigits = len - start;
315 // We currently insert a digit separator every three digits.
316 final int nSeparators = (nDigits - 1) / 3;
317 synchronized(mWidthLock) {
318 // Always return an upper bound, even in the presence of rounding errors.
319 return nSeparators * mGroupingSeparatorWidthRatio;
320 }
321 }
322
323 /**
324 * Return extra width credit for absence of ellipsis, as fraction of a digit width.
325 * May be called by non-UI thread.
326 */
327 public float getNoEllipsisCredit() {
328 synchronized(mWidthLock) {
329 return mNoEllipsisCredit;
330 }
331 }
332
333 /**
334 * Return extra width credit for presence of a decimal point, as fraction of a digit width.
335 * May be called by non-UI thread.
336 */
337 public float getDecimalCredit() {
338 synchronized(mWidthLock) {
339 return mDecimalCredit;
340 }
341 }
342
Hans Boehma0e45f32015-05-30 13:20:35 -0700343 // Return the length of the exponent representation for the given exponent, in
344 // characters.
345 private final int expLen(int exp) {
346 if (exp == 0) return 0;
Hans Boehm5e802f32015-06-22 17:18:52 -0700347 final int abs_exp_digits = (int) Math.ceil(Math.log10(Math.abs((double)exp))
348 + 0.0000000001d /* Round whole numbers to next integer */);
349 return abs_exp_digits + (exp >= 0 ? 1 : 2);
Hans Boehm61568a12015-05-18 18:25:41 -0700350 }
351
Hans Boehma0e45f32015-05-30 13:20:35 -0700352 /**
353 * Initiate display of a new result.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700354 * Only called from UI thread.
Hans Boehma0e45f32015-05-30 13:20:35 -0700355 * The parameters specify various properties of the result.
356 * @param initPrec Initial display precision computed by evaluator. (1 = tenths digit)
357 * @param msd Position of most significant digit. Offset from left of string.
358 Evaluator.INVALID_MSD if unknown.
359 * @param leastDigPos Position of least significant digit (1 = tenths digit)
360 * or Integer.MAX_VALUE.
361 * @param truncatedWholePart Result up to but not including decimal point.
362 Currently we only use the length.
363 */
364 void displayResult(int initPrec, int msd, int leastDigPos, String truncatedWholePart) {
365 initPositions(initPrec, msd, leastDigPos, truncatedWholePart);
Hans Boehm84614952014-11-25 18:46:17 -0800366 redisplay();
367 }
368
Hans Boehma0e45f32015-05-30 13:20:35 -0700369 /**
Hans Boehm5e802f32015-06-22 17:18:52 -0700370 * Set up scroll bounds (mMinPos, mMaxPos, etc.) and determine whether the result is
371 * scrollable, based on the supplied information about the result.
Hans Boehma0e45f32015-05-30 13:20:35 -0700372 * This is unfortunately complicated because we need to predict whether trailing digits
373 * will eventually be replaced by an exponent.
374 * Just appending the exponent during formatting would be simpler, but would produce
375 * jumpier results during transitions.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700376 * Only called from UI thread.
Hans Boehma0e45f32015-05-30 13:20:35 -0700377 */
Hans Boehm5e802f32015-06-22 17:18:52 -0700378 private void initPositions(int initPrecOffset, int msdIndex, int lsdOffset,
379 String truncatedWholePart) {
Hans Boehma0e45f32015-05-30 13:20:35 -0700380 int maxChars = getMaxChars();
Hans Boehm24c91ed2016-06-30 18:53:44 -0700381 mWholeLen = truncatedWholePart.length();
382 // Allow a tiny amount of slop for associativity/rounding differences in length
383 // calculation. If getPreferredPrec() decided it should fit, we want to make it fit, too.
384 // We reserved one extra pixel, so the extra length is OK.
385 final int nSeparatorChars = (int) Math.ceil(
386 separatorChars(truncatedWholePart, truncatedWholePart.length())
387 - getNoEllipsisCredit() - 0.0001f);
388 mWholePartFits = mWholeLen + nSeparatorChars <= maxChars;
Hans Boehma0e45f32015-05-30 13:20:35 -0700389 mLastPos = INVALID;
Hans Boehm5e802f32015-06-22 17:18:52 -0700390 mLsdOffset = lsdOffset;
Hans Boehm24c91ed2016-06-30 18:53:44 -0700391 mAppendExponent = false;
Hans Boehma0e45f32015-05-30 13:20:35 -0700392 // Prevent scrolling past initial position, which is calculated to show leading digits.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700393 mCurrentPos = mMinPos = (int) Math.round(initPrecOffset * mCharWidth);
Hans Boehm5e802f32015-06-22 17:18:52 -0700394 if (msdIndex == Evaluator.INVALID_MSD) {
Hans Boehma0e45f32015-05-30 13:20:35 -0700395 // Possible zero value
Hans Boehm5e802f32015-06-22 17:18:52 -0700396 if (lsdOffset == Integer.MIN_VALUE) {
Hans Boehma0e45f32015-05-30 13:20:35 -0700397 // Definite zero value.
398 mMaxPos = mMinPos;
Hans Boehm24c91ed2016-06-30 18:53:44 -0700399 mMaxCharOffset = (int) Math.round(mMaxPos/mCharWidth);
Hans Boehma0e45f32015-05-30 13:20:35 -0700400 mScrollable = false;
401 } else {
402 // May be very small nonzero value. Allow user to find out.
Hans Boehm5e802f32015-06-22 17:18:52 -0700403 mMaxPos = mMaxCharOffset = MAX_RIGHT_SCROLL;
Hans Boehm24c91ed2016-06-30 18:53:44 -0700404 mMinPos -= mCharWidth; // Allow for future minus sign.
Hans Boehma0e45f32015-05-30 13:20:35 -0700405 mScrollable = true;
406 }
407 return;
408 }
Hans Boehma0e45f32015-05-30 13:20:35 -0700409 int negative = truncatedWholePart.charAt(0) == '-' ? 1 : 0;
Hans Boehm65a99a42016-02-03 18:16:07 -0800410 if (msdIndex > mWholeLen && msdIndex <= mWholeLen + 3) {
Hans Boehm5e802f32015-06-22 17:18:52 -0700411 // Avoid tiny negative exponent; pretend msdIndex is just to the right of decimal point.
Hans Boehm65a99a42016-02-03 18:16:07 -0800412 msdIndex = mWholeLen - 1;
Hans Boehma0e45f32015-05-30 13:20:35 -0700413 }
Hans Boehm24c91ed2016-06-30 18:53:44 -0700414 // Set to position of leftmost significant digit relative to dec. point. Usually negative.
Hans Boehm65a99a42016-02-03 18:16:07 -0800415 int minCharOffset = msdIndex - mWholeLen;
Hans Boehm5e802f32015-06-22 17:18:52 -0700416 if (minCharOffset > -1 && minCharOffset < MAX_LEADING_ZEROES + 2) {
Hans Boehma0e45f32015-05-30 13:20:35 -0700417 // Small number of leading zeroes, avoid scientific notation.
Hans Boehm5e802f32015-06-22 17:18:52 -0700418 minCharOffset = -1;
Hans Boehma0e45f32015-05-30 13:20:35 -0700419 }
Hans Boehm5e802f32015-06-22 17:18:52 -0700420 if (lsdOffset < MAX_RIGHT_SCROLL) {
421 mMaxCharOffset = lsdOffset;
422 if (mMaxCharOffset < -1 && mMaxCharOffset > -(MAX_TRAILING_ZEROES + 2)) {
423 mMaxCharOffset = -1;
Hans Boehma0e45f32015-05-30 13:20:35 -0700424 }
Hans Boehm5e802f32015-06-22 17:18:52 -0700425 // lsdOffset is positive or negative, never 0.
426 int currentExpLen = 0; // Length of required standard scientific notation exponent.
427 if (mMaxCharOffset < -1) {
428 currentExpLen = expLen(-minCharOffset - 1);
429 } else if (minCharOffset > -1 || mMaxCharOffset >= maxChars) {
Hans Boehm24c91ed2016-06-30 18:53:44 -0700430 // Number is either entirely to the right of decimal point, or decimal point is
431 // not visible when scrolled to the right.
Hans Boehm5e802f32015-06-22 17:18:52 -0700432 currentExpLen = expLen(-minCharOffset);
Hans Boehma0e45f32015-05-30 13:20:35 -0700433 }
Hans Boehm24c91ed2016-06-30 18:53:44 -0700434 // Exponent length does not included added decimal point. But whenever we add a
435 // decimal point, we allow an extra character (SCI_NOTATION_EXTRA).
436 final int separatorLength = mWholePartFits && minCharOffset < -3 ? nSeparatorChars : 0;
437 mScrollable = (mMaxCharOffset + currentExpLen + separatorLength - minCharOffset
438 + negative >= maxChars);
439 // Now adjust mMaxCharOffset for any required exponent.
Hans Boehm5e802f32015-06-22 17:18:52 -0700440 int newMaxCharOffset;
441 if (currentExpLen > 0) {
442 if (mScrollable) {
443 // We'll use exponent corresponding to leastDigPos when scrolled to right.
444 newMaxCharOffset = mMaxCharOffset + expLen(-lsdOffset);
445 } else {
446 newMaxCharOffset = mMaxCharOffset + currentExpLen;
447 }
448 if (mMaxCharOffset <= -1 && newMaxCharOffset > -1) {
449 // Very unlikely; just drop exponent.
450 mMaxCharOffset = -1;
451 } else {
Hans Boehm24c91ed2016-06-30 18:53:44 -0700452 mMaxCharOffset = Math.min(newMaxCharOffset, MAX_RIGHT_SCROLL);
Hans Boehma0e45f32015-05-30 13:20:35 -0700453 }
Hans Boehm24c91ed2016-06-30 18:53:44 -0700454 mMaxPos = Math.min((int) Math.round(mMaxCharOffset * mCharWidth),
455 MAX_RIGHT_SCROLL);
456 } else if (!mWholePartFits && !mScrollable) {
457 // Corner case in which entire number fits, but not with grouping separators. We
458 // will use an exponent in un-scrolled position, which may hide digits. Scrolling
459 // by one character will remove the exponent and reveal the last digits. Note
460 // that in the forced scientific notation case, the exponent length is not
461 // factored into mMaxCharOffset, since we do not want such an increase to impact
462 // scrolling behavior. In the unscrollable case, we thus have to append the
463 // exponent at the end using the forcePrecision argument to formatResult, in order
464 // to ensure that we get the entire result.
465 mScrollable = (mMaxCharOffset + expLen(-minCharOffset - 1) - minCharOffset
466 + negative >= maxChars);
467 if (mScrollable) {
468 mMaxPos = (int) Math.ceil(mMinPos + mCharWidth);
469 // Single character scroll will remove exponent and show remaining piece.
470 } else {
471 mMaxPos = mMinPos;
472 mAppendExponent = true;
473 }
474 } else {
475 mMaxPos = Math.min((int) Math.round(mMaxCharOffset * mCharWidth),
476 MAX_RIGHT_SCROLL);
Hans Boehma0e45f32015-05-30 13:20:35 -0700477 }
Hans Boehma0e45f32015-05-30 13:20:35 -0700478 if (!mScrollable) {
479 // Position the number consistently with our assumptions to make sure it
480 // actually fits.
481 mCurrentPos = mMaxPos;
482 }
483 } else {
Hans Boehm5e802f32015-06-22 17:18:52 -0700484 mMaxPos = mMaxCharOffset = MAX_RIGHT_SCROLL;
Hans Boehma0e45f32015-05-30 13:20:35 -0700485 mScrollable = true;
486 }
487 }
488
Hans Boehm24c91ed2016-06-30 18:53:44 -0700489 /**
490 * Display error message indicated by resourceId.
491 * UI thread only.
492 */
Hans Boehm84614952014-11-25 18:46:17 -0800493 void displayError(int resourceId) {
Hans Boehm760a9dc2015-04-20 10:27:12 -0700494 mValid = true;
Hans Boehm84614952014-11-25 18:46:17 -0800495 mScrollable = false;
Hans Boehm14344ff2016-06-08 13:01:51 -0700496 final String msg = getContext().getString(resourceId);
Hans Boehm14344ff2016-06-08 13:01:51 -0700497 final float measuredWidth = Layout.getDesiredWidth(msg, getPaint());
Hans Boehm24c91ed2016-06-30 18:53:44 -0700498 if (measuredWidth > mWidthConstraint) {
Hans Boehm14344ff2016-06-08 13:01:51 -0700499 // Multiply by .99 to avoid rounding effects.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700500 final float scaleFactor = 0.99f * mWidthConstraint / measuredWidth;
Hans Boehm14344ff2016-06-08 13:01:51 -0700501 final RelativeSizeSpan smallTextSpan = new RelativeSizeSpan(scaleFactor);
502 final SpannableString scaledMsg = new SpannableString(msg);
503 scaledMsg.setSpan(smallTextSpan, 0, msg.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
504 setText(scaledMsg);
505 } else {
506 setText(msg);
507 }
Hans Boehm84614952014-11-25 18:46:17 -0800508 }
509
Hans Boehm013969e2015-04-13 20:29:47 -0700510 private final int MAX_COPY_SIZE = 1000000;
511
Hans Boehma0e45f32015-05-30 13:20:35 -0700512 /*
513 * Return the most significant digit position in the given string or Evaluator.INVALID_MSD.
Hans Boehm3666e632015-07-27 18:33:12 -0700514 * Unlike Evaluator.getMsdIndexOf, we treat a final 1 as significant.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700515 * Pure function; callable from anywhere.
Hans Boehma0e45f32015-05-30 13:20:35 -0700516 */
Hans Boehm3666e632015-07-27 18:33:12 -0700517 public static int getNaiveMsdIndexOf(String s) {
Hans Boehm65a99a42016-02-03 18:16:07 -0800518 final int len = s.length();
Hans Boehma0e45f32015-05-30 13:20:35 -0700519 for (int i = 0; i < len; ++i) {
520 char c = s.charAt(i);
521 if (c != '-' && c != '.' && c != '0') {
522 return i;
523 }
524 }
525 return Evaluator.INVALID_MSD;
526 }
527
Hans Boehm24c91ed2016-06-30 18:53:44 -0700528 /**
529 * Format a result returned by Evaluator.getString() into a single line containing ellipses
530 * (if appropriate) and an exponent (if appropriate).
531 * We add two distinct kinds of exponents:
532 * (1) If the final result contains the leading digit we use standard scientific notation.
533 * (2) If not, we add an exponent corresponding to an interpretation of the final result as
534 * an integer.
535 * We add an ellipsis on the left if the result was truncated.
536 * We add ellipses and exponents in a way that leaves most digits in the position they
537 * would have been in had we not done so. This minimizes jumps as a result of scrolling.
538 * Result is NOT internationalized, uses "E" for exponent.
539 * Called only from UI thread; We sometimes omit locking for fields.
540 * @param precOffset The value that was passed to getString. Identifies the significance of
541 the rightmost digit. A value of 1 means the rightmost digits corresponds to tenths.
542 * @param maxDigs The maximum number of characters in the result
543 * @param truncated The in parameter was already truncated, beyond possibly removing the
544 minus sign.
545 * @param negative The in parameter represents a negative result. (Minus sign may be removed
546 without setting truncated.)
547 * @param lastDisplayedOffset If not null, we set lastDisplayedOffset[0] to the offset of
548 the last digit actually appearing in the display.
549 * @param forcePrecision If true, we make sure that the last displayed digit corresponds to
550 precOffset, and allow maxDigs to be exceeded in adding the exponent and commas.
551 * @param forceSciNotation Force scientific notation. May be set because we don't have
552 space for grouping separators, but whole number otherwise fits.
553 * @param insertCommas Insert commas (literally, not internationalized) as digit separators.
554 We only ever do this for the integral part of a number, and only when no
555 exponent is displayed in the initial position. The combination of which means
556 that we only do it when no exponent is displayed.
557 We insert commas in a way that does consider the width of the actual localized digit
558 separator. Commas count towards maxDigs as the appropriate fraction of a digit.
559 */
560 private String formatResult(String in, int precOffset, int maxDigs, boolean truncated,
561 boolean negative, int lastDisplayedOffset[], boolean forcePrecision,
562 boolean forceSciNotation, boolean insertCommas) {
Hans Boehm5e802f32015-06-22 17:18:52 -0700563 final int minusSpace = negative ? 1 : 0;
Hans Boehm3666e632015-07-27 18:33:12 -0700564 final int msdIndex = truncated ? -1 : getNaiveMsdIndexOf(in); // INVALID_MSD is OK.
Hans Boehm5e802f32015-06-22 17:18:52 -0700565 String result = in;
Hans Boehm24c91ed2016-06-30 18:53:44 -0700566 boolean needEllipsis = false;
Hans Boehm73ecff22015-09-03 16:04:50 -0700567 if (truncated || (negative && result.charAt(0) != '-')) {
Hans Boehm24c91ed2016-06-30 18:53:44 -0700568 needEllipsis = true;
Hans Boehm73ecff22015-09-03 16:04:50 -0700569 result = KeyMaps.ELLIPSIS + result.substring(1, result.length());
570 // Ellipsis may be removed again in the type(1) scientific notation case.
571 }
572 final int decIndex = result.indexOf('.');
Hans Boehm65a99a42016-02-03 18:16:07 -0800573 if (lastDisplayedOffset != null) {
574 lastDisplayedOffset[0] = precOffset;
575 }
Hans Boehm24c91ed2016-06-30 18:53:44 -0700576 if (forceSciNotation || (decIndex == -1 || msdIndex != Evaluator.INVALID_MSD
Hans Boehm5e802f32015-06-22 17:18:52 -0700577 && msdIndex - decIndex > MAX_LEADING_ZEROES + 1) && precOffset != -1) {
Hans Boehm24c91ed2016-06-30 18:53:44 -0700578 // Either:
579 // 1) No decimal point displayed, and it's not just to the right of the last digit, or
580 // 2) we are at the front of a number whos integral part is too large to allow
581 // comma insertion, or
582 // 3) we should suppress leading zeroes.
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700583 // Add an exponent to let the user track which digits are currently displayed.
Hans Boehm5e802f32015-06-22 17:18:52 -0700584 // Start with type (2) exponent if we dropped no digits. -1 accounts for decimal point.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700585 // We currently never show digit separators together with an exponent.
Hans Boehm5e802f32015-06-22 17:18:52 -0700586 final int initExponent = precOffset > 0 ? -precOffset : -precOffset - 1;
587 int exponent = initExponent;
Hans Boehm08e8f322015-04-21 13:18:38 -0700588 boolean hasPoint = false;
Hans Boehm5e802f32015-06-22 17:18:52 -0700589 if (!truncated && msdIndex < maxDigs - 1
590 && result.length() - msdIndex + 1 + minusSpace
591 <= maxDigs + SCI_NOTATION_EXTRA) {
592 // Type (1) exponent computation and transformation:
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700593 // Leading digit is in display window. Use standard calculator scientific notation
594 // with one digit to the left of the decimal point. Insert decimal point and
595 // delete leading zeroes.
Hans Boehma0e45f32015-05-30 13:20:35 -0700596 // We try to keep leading digits roughly in position, and never
Hans Boehmf6dae112015-06-18 17:57:50 -0700597 // lengthen the result by more than SCI_NOTATION_EXTRA.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700598 if (decIndex > msdIndex) {
599 // In the forceSciNotation, we can have a decimal point in the relevant digit
600 // range. Remove it.
601 result = result.substring(0, decIndex)
602 + result.substring(decIndex + 1, result.length());
603 // msdIndex and precOffset unaffected.
604 }
Hans Boehm5e802f32015-06-22 17:18:52 -0700605 final int resLen = result.length();
606 String fraction = result.substring(msdIndex + 1, resLen);
607 result = (negative ? "-" : "") + result.substring(msdIndex, msdIndex + 1)
608 + "." + fraction;
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700609 // Original exp was correct for decimal point at right of fraction.
610 // Adjust by length of fraction.
Hans Boehm5e802f32015-06-22 17:18:52 -0700611 exponent = initExponent + resLen - msdIndex - 1;
Hans Boehm08e8f322015-04-21 13:18:38 -0700612 hasPoint = true;
613 }
Hans Boehm73ecff22015-09-03 16:04:50 -0700614 // Exponent can't be zero.
615 // Actually add the exponent of either type:
616 if (!forcePrecision) {
617 int dropDigits; // Digits to drop to make room for exponent.
618 if (hasPoint) {
619 // Type (1) exponent.
620 // Drop digits even if there is room. Otherwise the scrolling gets jumpy.
621 dropDigits = expLen(exponent);
622 if (dropDigits >= result.length() - 1) {
623 // Jumpy is better than no mantissa. Probably impossible anyway.
624 dropDigits = Math.max(result.length() - 2, 0);
Hans Boehma0e45f32015-05-30 13:20:35 -0700625 }
Hans Boehm73ecff22015-09-03 16:04:50 -0700626 } else {
627 // Type (2) exponent.
628 // Exponent depends on the number of digits we drop, which depends on
629 // exponent ...
630 for (dropDigits = 2; expLen(initExponent + dropDigits) > dropDigits;
631 ++dropDigits) {}
632 exponent = initExponent + dropDigits;
633 if (precOffset - dropDigits > mLsdOffset) {
634 // This can happen if e.g. result = 10^40 + 10^10
635 // It turns out we would otherwise display ...10e9 because it takes
636 // the same amount of space as ...1e10 but shows one more digit.
637 // But we don't want to display a trailing zero, even if it's free.
638 ++dropDigits;
639 ++exponent;
640 }
Hans Boehm08e8f322015-04-21 13:18:38 -0700641 }
Hans Boehm73ecff22015-09-03 16:04:50 -0700642 result = result.substring(0, result.length() - dropDigits);
Hans Boehm65a99a42016-02-03 18:16:07 -0800643 if (lastDisplayedOffset != null) {
644 lastDisplayedOffset[0] -= dropDigits;
645 }
Hans Boehm73ecff22015-09-03 16:04:50 -0700646 }
647 result = result + "E" + Integer.toString(exponent);
Hans Boehm24c91ed2016-06-30 18:53:44 -0700648 } else if (insertCommas) {
649 // Add commas to the whole number section, and then truncate on left to fit,
650 // counting commas as a fractional digit.
651 final int wholeStart = needEllipsis ? 1 : 0;
652 int orig_length = result.length();
653 final float nCommaChars;
654 if (decIndex != -1) {
655 nCommaChars = separatorChars(result, decIndex);
656 result = StringUtils.addCommas(result, wholeStart, decIndex)
657 + result.substring(decIndex, orig_length);
658 } else {
659 nCommaChars = separatorChars(result, orig_length);
660 result = StringUtils.addCommas(result, wholeStart, orig_length);
661 }
662 if (needEllipsis) {
663 orig_length -= 1; // Exclude ellipsis.
664 }
665 final float len = orig_length + nCommaChars;
666 int deletedChars = 0;
667 final float ellipsisCredit = getNoEllipsisCredit();
668 final float decimalCredit = getDecimalCredit();
669 final float effectiveLen = len - (decIndex == -1 ? 0 : getDecimalCredit());
670 final float ellipsisAdjustment =
671 needEllipsis ? mNoExponentCredit : getNoEllipsisCredit();
672 // As above, we allow for a tiny amount of extra length here, for consistency with
673 // getPreferredPrec().
674 if (effectiveLen - ellipsisAdjustment > (float) (maxDigs - wholeStart) + 0.0001f
675 && !forcePrecision) {
676 float deletedWidth = 0.0f;
677 while (effectiveLen - mNoExponentCredit - deletedWidth
678 > (float) (maxDigs - 1 /* for ellipsis */)) {
679 if (result.charAt(deletedChars) == ',') {
680 deletedWidth += mGroupingSeparatorWidthRatio;
681 } else {
682 deletedWidth += 1.0f;
683 }
684 deletedChars++;
685 }
686 }
687 if (deletedChars > 0) {
688 result = KeyMaps.ELLIPSIS + result.substring(deletedChars, result.length());
689 } else if (needEllipsis) {
690 result = KeyMaps.ELLIPSIS + result;
691 }
Hans Boehm5e802f32015-06-22 17:18:52 -0700692 }
693 return result;
Hans Boehm08e8f322015-04-21 13:18:38 -0700694 }
695
Hans Boehmf6dae112015-06-18 17:57:50 -0700696 /**
697 * Get formatted, but not internationalized, result from mEvaluator.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700698 * @param precOffset requested position (1 = tenths) of last included digit
699 * @param maxSize maximum number of characters (more or less) in result
700 * @param lastDisplayedOffset zeroth entry is set to actual offset of last included digit,
Hans Boehm65a99a42016-02-03 18:16:07 -0800701 * after adjusting for exponent, etc. May be null.
Hans Boehmf6dae112015-06-18 17:57:50 -0700702 * @param forcePrecision Ensure that last included digit is at pos, at the expense
703 * of treating maxSize as a soft limit.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700704 * @param forceSciNotation Force scientific notation, even if not required by maxSize.
705 * @param insertCommas Insert commas as digit separators.
Hans Boehmf6dae112015-06-18 17:57:50 -0700706 */
Hans Boehm5e802f32015-06-22 17:18:52 -0700707 private String getFormattedResult(int precOffset, int maxSize, int lastDisplayedOffset[],
Hans Boehm24c91ed2016-06-30 18:53:44 -0700708 boolean forcePrecision, boolean forceSciNotation, boolean insertCommas) {
Hans Boehm08e8f322015-04-21 13:18:38 -0700709 final boolean truncated[] = new boolean[1];
710 final boolean negative[] = new boolean[1];
Hans Boehm5e802f32015-06-22 17:18:52 -0700711 final int requestedPrecOffset[] = {precOffset};
712 final String rawResult = mEvaluator.getString(requestedPrecOffset, mMaxCharOffset,
Hans Boehma0e45f32015-05-30 13:20:35 -0700713 maxSize, truncated, negative);
Hans Boehm5e802f32015-06-22 17:18:52 -0700714 return formatResult(rawResult, requestedPrecOffset[0], maxSize, truncated[0], negative[0],
Hans Boehm24c91ed2016-06-30 18:53:44 -0700715 lastDisplayedOffset, forcePrecision, forceSciNotation, insertCommas);
Hans Boehm08e8f322015-04-21 13:18:38 -0700716 }
717
Hans Boehm65a99a42016-02-03 18:16:07 -0800718 /**
719 * Return entire result (within reason) up to current displayed precision.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700720 * @param withSeparators Add digit separators
Hans Boehm65a99a42016-02-03 18:16:07 -0800721 */
Hans Boehm24c91ed2016-06-30 18:53:44 -0700722 public String getFullText(boolean withSeparators) {
Hans Boehm760a9dc2015-04-20 10:27:12 -0700723 if (!mValid) return "";
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700724 if (!mScrollable) return getText().toString();
Hans Boehm5e802f32015-06-22 17:18:52 -0700725 return KeyMaps.translateResult(getFormattedResult(mLastDisplayedOffset, MAX_COPY_SIZE,
Hans Boehm24c91ed2016-06-30 18:53:44 -0700726 null, true /* forcePrecision */, false /* forceSciNotation */, withSeparators));
Hans Boehm84614952014-11-25 18:46:17 -0800727 }
728
Hans Boehm24c91ed2016-06-30 18:53:44 -0700729 /**
730 * Did the above produce a correct result?
731 * UI thread only.
732 */
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700733 public boolean fullTextIsExact() {
Hans Boehm24c91ed2016-06-30 18:53:44 -0700734 return !mScrollable || (mMaxCharOffset == getCharOffset(mCurrentPos)
735 && mMaxCharOffset != MAX_RIGHT_SCROLL);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700736 }
737
Hans Boehm61568a12015-05-18 18:25:41 -0700738 /**
Hans Boehm65a99a42016-02-03 18:16:07 -0800739 * Get entire result up to current displayed precision, or up to MAX_COPY_EXTRA additional
740 * digits, if it will lead to an exact result.
741 */
742 public String getFullCopyText() {
743 if (!mValid
744 || mLsdOffset == Integer.MAX_VALUE
745 || fullTextIsExact()
746 || mWholeLen > MAX_RECOMPUTE_DIGITS
747 || mWholeLen + mLsdOffset > MAX_RECOMPUTE_DIGITS
748 || mLsdOffset - mLastDisplayedOffset > MAX_COPY_EXTRA) {
Hans Boehm24c91ed2016-06-30 18:53:44 -0700749 return getFullText(false /* withSeparators */);
Hans Boehm65a99a42016-02-03 18:16:07 -0800750 }
751 // It's reasonable to compute and copy the exact result instead.
752 final int nonNegLsdOffset = Math.max(0, mLsdOffset);
Hans Boehm995e5eb2016-02-08 11:03:01 -0800753 final String rawResult = mEvaluator.getResult().toStringTruncated(nonNegLsdOffset);
Hans Boehm65a99a42016-02-03 18:16:07 -0800754 final String formattedResult = formatResult(rawResult, nonNegLsdOffset, MAX_COPY_SIZE,
Hans Boehm24c91ed2016-06-30 18:53:44 -0700755 false, rawResult.charAt(0) == '-', null, true /* forcePrecision */,
756 false /* forceSciNotation */, false /* insertCommas */);
Hans Boehm65a99a42016-02-03 18:16:07 -0800757 return KeyMaps.translateResult(formattedResult);
758 }
759
760 /**
Hans Boehm61568a12015-05-18 18:25:41 -0700761 * Return the maximum number of characters that will fit in the result display.
762 * May be called asynchronously from non-UI thread.
763 */
Hans Boehm84614952014-11-25 18:46:17 -0800764 int getMaxChars() {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700765 int result;
766 synchronized(mWidthLock) {
Justin Klaassen44595162015-05-28 17:55:20 -0700767 result = (int) Math.floor(mWidthConstraint / mCharWidth);
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700768 // We can apparently finish evaluating before onMeasure in CalculatorText has been
769 // called, in which case we get 0 or -1 as the width constraint.
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700770 }
Hans Boehm84614952014-11-25 18:46:17 -0800771 if (result <= 0) {
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700772 // Return something conservatively big, to force sufficient evaluation.
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700773 return MAX_WIDTH;
Hans Boehm84614952014-11-25 18:46:17 -0800774 } else {
Hans Boehm80018c82015-08-02 16:59:07 -0700775 return result;
Hans Boehm84614952014-11-25 18:46:17 -0800776 }
777 }
778
Hans Boehm61568a12015-05-18 18:25:41 -0700779 /**
Justin Klaassen44595162015-05-28 17:55:20 -0700780 * @return {@code true} if the currently displayed result is scrollable
Hans Boehm61568a12015-05-18 18:25:41 -0700781 */
Justin Klaassen44595162015-05-28 17:55:20 -0700782 public boolean isScrollable() {
783 return mScrollable;
Hans Boehm61568a12015-05-18 18:25:41 -0700784 }
785
Hans Boehm24c91ed2016-06-30 18:53:44 -0700786 /**
787 * Map pixel position to digit offset.
788 * UI thread only.
789 */
790 int getCharOffset(int pos) {
791 return (int) Math.round(pos / mCharWidth); // Lock not needed.
Hans Boehm013969e2015-04-13 20:29:47 -0700792 }
793
Hans Boehm84614952014-11-25 18:46:17 -0800794 void clear() {
Hans Boehm760a9dc2015-04-20 10:27:12 -0700795 mValid = false;
Hans Boehm1176f232015-05-11 16:26:03 -0700796 mScrollable = false;
Hans Boehm84614952014-11-25 18:46:17 -0800797 setText("");
798 }
799
Hans Boehm24c91ed2016-06-30 18:53:44 -0700800 /**
801 * Refresh display.
802 * Only called in UI thread.
803 */
Hans Boehm84614952014-11-25 18:46:17 -0800804 void redisplay() {
Hans Boehm24c91ed2016-06-30 18:53:44 -0700805 int currentCharOffset = getCharOffset(mCurrentPos);
Hans Boehm84614952014-11-25 18:46:17 -0800806 int maxChars = getMaxChars();
Hans Boehm5e802f32015-06-22 17:18:52 -0700807 int lastDisplayedOffset[] = new int[1];
Hans Boehm24c91ed2016-06-30 18:53:44 -0700808 String result = getFormattedResult(currentCharOffset, maxChars, lastDisplayedOffset,
809 mAppendExponent /* forcePrecision; preserve entire result */,
810 !mWholePartFits
811 && currentCharOffset == getCharOffset(mMinPos) /* forceSciNotation */,
812 mWholePartFits /* insertCommas */ );
Hans Boehm0b9806f2015-06-29 16:07:15 -0700813 int expIndex = result.indexOf('E');
Hans Boehm013969e2015-04-13 20:29:47 -0700814 result = KeyMaps.translateResult(result);
Hans Boehm5e802f32015-06-22 17:18:52 -0700815 if (expIndex > 0 && result.indexOf('.') == -1) {
Hans Boehm84614952014-11-25 18:46:17 -0800816 // Gray out exponent if used as position indicator
817 SpannableString formattedResult = new SpannableString(result);
Hans Boehm5e802f32015-06-22 17:18:52 -0700818 formattedResult.setSpan(mExponentColorSpan, expIndex, result.length(),
Hans Boehm84614952014-11-25 18:46:17 -0800819 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
820 setText(formattedResult);
821 } else {
822 setText(result);
823 }
Hans Boehm5e802f32015-06-22 17:18:52 -0700824 mLastDisplayedOffset = lastDisplayedOffset[0];
Hans Boehm760a9dc2015-04-20 10:27:12 -0700825 mValid = true;
Hans Boehm84614952014-11-25 18:46:17 -0800826 }
827
828 @Override
829 public void computeScroll() {
830 if (!mScrollable) return;
831 if (mScroller.computeScrollOffset()) {
832 mCurrentPos = mScroller.getCurrX();
Hans Boehm24c91ed2016-06-30 18:53:44 -0700833 if (getCharOffset(mCurrentPos) != getCharOffset(mLastPos)) {
Hans Boehm84614952014-11-25 18:46:17 -0800834 mLastPos = mCurrentPos;
835 redisplay();
836 }
837 if (!mScroller.isFinished()) {
Justin Klaassen44595162015-05-28 17:55:20 -0700838 postInvalidateOnAnimation();
Hans Boehm84614952014-11-25 18:46:17 -0800839 }
840 }
841 }
842
Chenjie Yu3937b652016-06-01 23:14:26 -0700843 /**
844 * Use ActionMode for copy support on M and higher.
845 */
846 @TargetApi(Build.VERSION_CODES.M)
847 private void setupActionMode() {
848 mCopyActionModeCallback = new ActionMode.Callback2() {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700849
Chenjie Yu3937b652016-06-01 23:14:26 -0700850 @Override
851 public boolean onCreateActionMode(ActionMode mode, Menu menu) {
852 final MenuInflater inflater = mode.getMenuInflater();
853 return createCopyMenu(inflater, menu);
854 }
Hans Boehm7f83e362015-06-10 15:41:04 -0700855
Chenjie Yu3937b652016-06-01 23:14:26 -0700856 @Override
857 public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
858 return false; // Return false if nothing is done
859 }
Hans Boehm7f83e362015-06-10 15:41:04 -0700860
Chenjie Yu3937b652016-06-01 23:14:26 -0700861 @Override
862 public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
863 if (onMenuItemClick(item)) {
Hans Boehm65a99a42016-02-03 18:16:07 -0800864 mode.finish();
865 return true;
Chenjie Yu3937b652016-06-01 23:14:26 -0700866 } else {
867 return false;
Hans Boehm65a99a42016-02-03 18:16:07 -0800868 }
Chenjie Yu3937b652016-06-01 23:14:26 -0700869 }
870
871 @Override
872 public void onDestroyActionMode(ActionMode mode) {
873 unhighlightResult();
874 mActionMode = null;
875 }
876
877 @Override
878 public void onGetContentRect(ActionMode mode, View view, Rect outRect) {
879 super.onGetContentRect(mode, view, outRect);
880
881 outRect.left += view.getPaddingLeft();
882 outRect.top += view.getPaddingTop();
883 outRect.right -= view.getPaddingRight();
884 outRect.bottom -= view.getPaddingBottom();
885 final int width = (int) Layout.getDesiredWidth(getText(), getPaint());
886 if (width < outRect.width()) {
887 outRect.left = outRect.right - width;
888 }
889
890 if (!BuildCompat.isAtLeastN()) {
891 // The CAB (prior to N) only takes the translation of a view into account, so
892 // if a scale is applied to the view then the offset outRect will end up being
893 // positioned incorrectly. We workaround that limitation by manually applying
894 // the scale to the outRect, which the CAB will then offset to the correct
895 // position.
896 final float scaleX = view.getScaleX();
897 final float scaleY = view.getScaleY();
898 outRect.left *= scaleX;
899 outRect.right *= scaleX;
900 outRect.top *= scaleY;
901 outRect.bottom *= scaleY;
902 }
903 }
904 };
905 setOnLongClickListener(new View.OnLongClickListener() {
906 @Override
907 public boolean onLongClick(View v) {
908 if (mValid) {
909 mActionMode = startActionMode(mCopyActionModeCallback,
910 ActionMode.TYPE_FLOATING);
911 return true;
912 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700913 return false;
914 }
Chenjie Yu3937b652016-06-01 23:14:26 -0700915 });
916 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700917
Chenjie Yu3937b652016-06-01 23:14:26 -0700918 /**
919 * Use ContextMenu for copy support on L and lower.
920 */
921 private void setupContextMenu() {
922 setOnCreateContextMenuListener(new OnCreateContextMenuListener() {
923 @Override
924 public void onCreateContextMenu(ContextMenu contextMenu, View view,
925 ContextMenu.ContextMenuInfo contextMenuInfo) {
926 final MenuInflater inflater = new MenuInflater(getContext());
927 createCopyMenu(inflater, contextMenu);
928 mContextMenu = contextMenu;
929 for(int i = 0; i < contextMenu.size(); i ++) {
930 contextMenu.getItem(i).setOnMenuItemClickListener(CalculatorResult.this);
931 }
Hans Boehm7f83e362015-06-10 15:41:04 -0700932 }
Chenjie Yu3937b652016-06-01 23:14:26 -0700933 });
934 setOnLongClickListener(new View.OnLongClickListener() {
935 @Override
936 public boolean onLongClick(View v) {
937 if (mValid) {
938 return showContextMenu();
939 }
940 return false;
Justin Klaassenf1b61f42016-04-27 16:00:11 -0700941 }
Chenjie Yu3937b652016-06-01 23:14:26 -0700942 });
943 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700944
Chenjie Yu3937b652016-06-01 23:14:26 -0700945 private boolean createCopyMenu(MenuInflater inflater, Menu menu) {
946 inflater.inflate(R.menu.copy, menu);
947 highlightResult();
948 return true;
949 }
950
951 public boolean stopActionModeOrContextMenu() {
Hans Boehm1176f232015-05-11 16:26:03 -0700952 if (mActionMode != null) {
953 mActionMode.finish();
954 return true;
955 }
Chenjie Yu3937b652016-06-01 23:14:26 -0700956 if (mContextMenu != null) {
957 unhighlightResult();
958 mContextMenu.close();
959 return true;
960 }
Hans Boehm1176f232015-05-11 16:26:03 -0700961 return false;
962 }
963
Chenjie Yu3937b652016-06-01 23:14:26 -0700964 private void highlightResult() {
965 final Spannable text = (Spannable) getText();
966 text.setSpan(mHighlightSpan, 0, text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
967 }
968
969 private void unhighlightResult() {
970 final Spannable text = (Spannable) getText();
971 text.removeSpan(mHighlightSpan);
972 }
973
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700974 private void setPrimaryClip(ClipData clip) {
975 ClipboardManager clipboard = (ClipboardManager) getContext().
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700976 getSystemService(Context.CLIPBOARD_SERVICE);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700977 clipboard.setPrimaryClip(clip);
978 }
979
980 private void copyContent() {
Hans Boehm65a99a42016-02-03 18:16:07 -0800981 final CharSequence text = getFullCopyText();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700982 ClipboardManager clipboard =
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700983 (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
984 // We include a tag URI, to allow us to recognize our own results and handle them
985 // specially.
986 ClipData.Item newItem = new ClipData.Item(text, null, mEvaluator.capture());
987 String[] mimeTypes = new String[] {ClipDescription.MIMETYPE_TEXT_PLAIN};
988 ClipData cd = new ClipData("calculator result", mimeTypes, newItem);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700989 clipboard.setPrimaryClip(cd);
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700990 Toast.makeText(getContext(), R.string.text_copied_toast, Toast.LENGTH_SHORT).show();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700991 }
992
Chenjie Yu3937b652016-06-01 23:14:26 -0700993 @Override
994 public boolean onMenuItemClick(MenuItem item) {
995 switch (item.getItemId()) {
996 case R.id.menu_copy:
997 if (mEvaluator.reevaluationInProgress()) {
998 // Refuse to copy placeholder characters.
999 return false;
1000 } else {
1001 copyContent();
1002 unhighlightResult();
1003 return true;
1004 }
1005 default:
1006 return false;
1007 }
1008 }
Hans Boehm84614952014-11-25 18:46:17 -08001009}