blob: 8fa698b94bf55eca2869f26a2c1c5e8598379cb9 [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;
Aurimas Liutikas8c43f062018-03-28 08:10:28 -070026import androidx.annotation.IntDef;
27import androidx.core.content.ContextCompat;
28import androidx.core.os.BuildCompat;
Justin Klaassen44595162015-05-28 17:55:20 -070029import android.text.Layout;
Hans Boehm7f83e362015-06-10 15:41:04 -070030import android.text.Spannable;
Hans Boehm84614952014-11-25 18:46:17 -080031import android.text.SpannableString;
Hans Boehm1176f232015-05-11 16:26:03 -070032import android.text.Spanned;
Justin Klaassen44595162015-05-28 17:55:20 -070033import android.text.TextPaint;
Hans Boehm7f83e362015-06-10 15:41:04 -070034import android.text.style.BackgroundColorSpan;
Hans Boehm84614952014-11-25 18:46:17 -080035import android.text.style.ForegroundColorSpan;
Hans Boehm14344ff2016-06-08 13:01:51 -070036import android.text.style.RelativeSizeSpan;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070037import android.util.AttributeSet;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070038import android.view.ActionMode;
Chenjie Yu3937b652016-06-01 23:14:26 -070039import android.view.ContextMenu;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070040import android.view.GestureDetector;
41import android.view.Menu;
42import android.view.MenuInflater;
43import android.view.MenuItem;
44import android.view.MotionEvent;
45import android.view.View;
Annie Chinc5b6e4f2016-12-05 13:34:14 -080046import android.view.ViewConfiguration;
Justin Klaassen44595162015-05-28 17:55:20 -070047import android.widget.OverScroller;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070048import android.widget.Toast;
Hans Boehm84614952014-11-25 18:46:17 -080049
Hans Boehmbd01e4b2016-11-23 10:12:58 -080050import java.lang.annotation.Retention;
51import java.lang.annotation.RetentionPolicy;
52
Hans Boehm84614952014-11-25 18:46:17 -080053// A text widget that is "infinitely" scrollable to the right,
54// and obtains the text to display via a callback to Logic.
Hans Boehm8f051c32016-10-03 16:53:58 -070055public class CalculatorResult extends AlignedTextView implements MenuItem.OnMenuItemClickListener,
56 Evaluator.EvaluationListener, Evaluator.CharMetricsInfo {
Hans Boehm61568a12015-05-18 18:25:41 -070057 static final int MAX_RIGHT_SCROLL = 10000000;
Hans Boehm08e8f322015-04-21 13:18:38 -070058 static final int INVALID = MAX_RIGHT_SCROLL + 10000;
Hans Boehm84614952014-11-25 18:46:17 -080059 // A larger value is unlikely to avoid running out of space
60 final OverScroller mScroller;
61 final GestureDetector mGestureDetector;
Hans Boehm8f051c32016-10-03 16:53:58 -070062 private long mIndex; // Index of expression we are displaying.
Hans Boehm84614952014-11-25 18:46:17 -080063 private Evaluator mEvaluator;
64 private boolean mScrollable = false;
65 // A scrollable result is currently displayed.
Hans Boehm760a9dc2015-04-20 10:27:12 -070066 private boolean mValid = false;
Annie Chin330419a2017-02-01 17:51:38 -080067 // The result holds a valid number (not an error message).
Hans Boehm5e802f32015-06-22 17:18:52 -070068 // A suffix of "Pos" denotes a pixel offset. Zero represents a scroll position
69 // in which the decimal point is just barely visible on the right of the display.
Hans Boehmc01cd7f2015-05-12 18:32:19 -070070 private int mCurrentPos;// Position of right of display relative to decimal point, in pixels.
71 // Large positive values mean the decimal point is scrolled off the
72 // left of the display. Zero means decimal point is barely displayed
73 // on the right.
Hans Boehm61568a12015-05-18 18:25:41 -070074 private int mLastPos; // Position already reflected in display. Pixels.
Hans Boehm65a99a42016-02-03 18:16:07 -080075 private int mMinPos; // Minimum position to avoid unnecessary blanks on the left. Pixels.
Hans Boehm61568a12015-05-18 18:25:41 -070076 private int mMaxPos; // Maximum position before we start displaying the infinite
77 // sequence of trailing zeroes on the right. Pixels.
Hans Boehm65a99a42016-02-03 18:16:07 -080078 private int mWholeLen; // Length of the whole part of current result.
Hans Boehm5e802f32015-06-22 17:18:52 -070079 // In the following, we use a suffix of Offset to denote a character position in a numeric
80 // string relative to the decimal point. Positive is to the right and negative is to
81 // the left. 1 = tenths position, -1 = units. Integer.MAX_VALUE is sometimes used
82 // for the offset of the last digit in an a nonterminating decimal expansion.
83 // We use the suffix "Index" to denote a zero-based index into a string representing a
84 // result.
Hans Boehm5e802f32015-06-22 17:18:52 -070085 private int mMaxCharOffset; // Character offset from decimal point of rightmost digit
Hans Boehm24c91ed2016-06-30 18:53:44 -070086 // that should be displayed, plus the length of any exponent
87 // needed to display that digit.
88 // Limited to MAX_RIGHT_SCROLL. Often the same as:
Hans Boehm5e802f32015-06-22 17:18:52 -070089 private int mLsdOffset; // Position of least-significant digit in result
90 private int mLastDisplayedOffset; // Offset of last digit actually displayed after adding
Hans Boehmf6dae112015-06-18 17:57:50 -070091 // exponent.
Hans Boehm24c91ed2016-06-30 18:53:44 -070092 private boolean mWholePartFits; // Scientific notation not needed for initial display.
93 private float mNoExponentCredit;
94 // Fraction of digit width saved by avoiding scientific notation.
95 // Only accessed from UI thread.
96 private boolean mAppendExponent;
97 // The result fits entirely in the display, even with an exponent,
98 // but not with grouping separators. Since the result is not
99 // scrollable, and we do not add the exponent to max. scroll position,
100 // append an exponent insteadd of replacing trailing digits.
Justin Klaassen44595162015-05-28 17:55:20 -0700101 private final Object mWidthLock = new Object();
Hans Boehm24c91ed2016-06-30 18:53:44 -0700102 // Protects the next five fields. These fields are only
Hans Boehm3465c782016-12-12 17:28:10 -0800103 // updated by the UI thread, and read accesses by the UI thread
Hans Boehm24c91ed2016-06-30 18:53:44 -0700104 // sometimes do not acquire the lock.
Hans Boehmd4959e82016-11-15 18:01:28 -0800105 private int mWidthConstraint = 0;
Hans Boehma0e45f32015-05-30 13:20:35 -0700106 // Our total width in pixels minus space for ellipsis.
Hans Boehmd4959e82016-11-15 18:01:28 -0800107 // 0 ==> uninitialized.
Justin Klaassen44595162015-05-28 17:55:20 -0700108 private float mCharWidth = 1;
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700109 // Maximum character width. For now we pretend that all characters
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700110 // have this width.
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700111 // TODO: We're not really using a fixed width font. But it appears
112 // to be close enough for the characters we use that the difference
113 // is not noticeable.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700114 private float mGroupingSeparatorWidthRatio;
115 // Fraction of digit width occupied by a digit separator.
116 private float mDecimalCredit;
117 // Fraction of digit width saved by replacing digit with decimal point.
118 private float mNoEllipsisCredit;
119 // Fraction of digit width saved by both replacing ellipsis with digit
120 // and avoiding scientific notation.
Hans Boehmbd01e4b2016-11-23 10:12:58 -0800121 @Retention(RetentionPolicy.SOURCE)
122 @IntDef({SHOULD_REQUIRE, SHOULD_EVALUATE, SHOULD_NOT_EVALUATE})
123 public @interface EvaluationRequest {}
124 public static final int SHOULD_REQUIRE = 2;
125 public static final int SHOULD_EVALUATE = 1;
126 public static final int SHOULD_NOT_EVALUATE = 0;
127 @EvaluationRequest private int mEvaluationRequest = SHOULD_REQUIRE;
128 // Should we evaluate when layout completes, and how?
Hans Boehmd4959e82016-11-15 18:01:28 -0800129 private Evaluator.EvaluationListener mEvaluationListener = this;
130 // Listener to use if/when evaluation is requested.
Hans Boehm50ed3202015-06-09 14:35:49 -0700131 public static final int MAX_LEADING_ZEROES = 6;
Hans Boehma0e45f32015-05-30 13:20:35 -0700132 // Maximum number of leading zeroes after decimal point before we
133 // switch to scientific notation with negative exponent.
Hans Boehm50ed3202015-06-09 14:35:49 -0700134 public static final int MAX_TRAILING_ZEROES = 6;
Hans Boehma0e45f32015-05-30 13:20:35 -0700135 // Maximum number of trailing zeroes before the decimal point before
136 // we switch to scientific notation with positive exponent.
137 private static final int SCI_NOTATION_EXTRA = 1;
138 // Extra digits for standard scientific notation. In this case we
Hans Boehm80018c82015-08-02 16:59:07 -0700139 // have a decimal point and no ellipsis.
140 // We assume that we do not drop digits to make room for the decimal
141 // point in ordinary scientific notation. Thus >= 1.
Hans Boehm65a99a42016-02-03 18:16:07 -0800142 private static final int MAX_COPY_EXTRA = 100;
143 // The number of extra digits we are willing to compute to copy
144 // a result as an exact number.
145 private static final int MAX_RECOMPUTE_DIGITS = 2000;
146 // The maximum number of digits we're willing to recompute in the UI
147 // thread. We only do this for known rational results, where we
148 // can bound the computation cost.
Chenjie Yu3937b652016-06-01 23:14:26 -0700149 private final ForegroundColorSpan mExponentColorSpan;
150 private final BackgroundColorSpan mHighlightSpan;
Hans Boehm65a99a42016-02-03 18:16:07 -0800151
Hans Boehm1176f232015-05-11 16:26:03 -0700152 private ActionMode mActionMode;
Chenjie Yu3937b652016-06-01 23:14:26 -0700153 private ActionMode.Callback mCopyActionModeCallback;
154 private ContextMenu mContextMenu;
Hans Boehm84614952014-11-25 18:46:17 -0800155
Annie Chin37c33b62016-11-22 14:46:28 -0800156 // The user requested that the result currently being evaluated should be stored to "memory".
157 private boolean mStoreToMemoryRequested = false;
158
Hans Boehm84614952014-11-25 18:46:17 -0800159 public CalculatorResult(Context context, AttributeSet attrs) {
160 super(context, attrs);
161 mScroller = new OverScroller(context);
Chenjie Yu3937b652016-06-01 23:14:26 -0700162 mHighlightSpan = new BackgroundColorSpan(getHighlightColor());
163 mExponentColorSpan = new ForegroundColorSpan(
164 ContextCompat.getColor(context, R.color.display_result_exponent_text_color));
Hans Boehm84614952014-11-25 18:46:17 -0800165 mGestureDetector = new GestureDetector(context,
166 new GestureDetector.SimpleOnGestureListener() {
167 @Override
Justin Klaassend48b7562015-04-16 16:51:38 -0700168 public boolean onDown(MotionEvent e) {
169 return true;
170 }
171 @Override
Annie Chinc5b6e4f2016-12-05 13:34:14 -0800172 public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
173 float velocityY) {
Hans Boehm84614952014-11-25 18:46:17 -0800174 if (!mScroller.isFinished()) {
175 mCurrentPos = mScroller.getFinalX();
176 }
177 mScroller.forceFinished(true);
Chenjie Yu3937b652016-06-01 23:14:26 -0700178 stopActionModeOrContextMenu();
Hans Boehmfbcef702015-04-27 18:07:47 -0700179 CalculatorResult.this.cancelLongPress();
180 // Ignore scrolls of error string, etc.
181 if (!mScrollable) return true;
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700182 mScroller.fling(mCurrentPos, 0, - (int) velocityX, 0 /* horizontal only */,
Hans Boehm61568a12015-05-18 18:25:41 -0700183 mMinPos, mMaxPos, 0, 0);
Justin Klaassen44595162015-05-28 17:55:20 -0700184 postInvalidateOnAnimation();
Hans Boehm84614952014-11-25 18:46:17 -0800185 return true;
186 }
187 @Override
Annie Chinc5b6e4f2016-12-05 13:34:14 -0800188 public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
189 float distanceY) {
Hans Boehm61568a12015-05-18 18:25:41 -0700190 int distance = (int)distanceX;
Hans Boehm84614952014-11-25 18:46:17 -0800191 if (!mScroller.isFinished()) {
192 mCurrentPos = mScroller.getFinalX();
193 }
194 mScroller.forceFinished(true);
Chenjie Yu3937b652016-06-01 23:14:26 -0700195 stopActionModeOrContextMenu();
Hans Boehm84614952014-11-25 18:46:17 -0800196 CalculatorResult.this.cancelLongPress();
197 if (!mScrollable) return true;
Hans Boehm61568a12015-05-18 18:25:41 -0700198 if (mCurrentPos + distance < mMinPos) {
199 distance = mMinPos - mCurrentPos;
200 } else if (mCurrentPos + distance > mMaxPos) {
201 distance = mMaxPos - mCurrentPos;
202 }
Hans Boehm84614952014-11-25 18:46:17 -0800203 int duration = (int)(e2.getEventTime() - e1.getEventTime());
204 if (duration < 1 || duration > 100) duration = 10;
Hans Boehm61568a12015-05-18 18:25:41 -0700205 mScroller.startScroll(mCurrentPos, 0, distance, 0, (int)duration);
Justin Klaassen44595162015-05-28 17:55:20 -0700206 postInvalidateOnAnimation();
Hans Boehm84614952014-11-25 18:46:17 -0800207 return true;
208 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700209 @Override
210 public void onLongPress(MotionEvent e) {
Hans Boehm1176f232015-05-11 16:26:03 -0700211 if (mValid) {
Justin Klaassen3a05c7e2016-03-04 12:40:02 -0800212 performLongClick();
Hans Boehm1176f232015-05-11 16:26:03 -0700213 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700214 }
Hans Boehm84614952014-11-25 18:46:17 -0800215 });
Annie Chin5e737532016-12-05 17:15:48 -0800216
217 final int slop = ViewConfiguration.get(context).getScaledTouchSlop();
Justin Klaassen3a05c7e2016-03-04 12:40:02 -0800218 setOnTouchListener(new View.OnTouchListener() {
Annie Chin5e737532016-12-05 17:15:48 -0800219
220 // Used to determine whether a touch event should be intercepted.
221 private float mInitialDownX;
222 private float mInitialDownY;
223
Justin Klaassen3a05c7e2016-03-04 12:40:02 -0800224 @Override
225 public boolean onTouch(View v, MotionEvent event) {
Annie Chinc5b6e4f2016-12-05 13:34:14 -0800226 final int action = event.getActionMasked();
227
228 final float x = event.getX();
229 final float y = event.getY();
230 switch (action) {
231 case MotionEvent.ACTION_DOWN:
232 mInitialDownX = x;
233 mInitialDownY = y;
234 break;
235 case MotionEvent.ACTION_MOVE:
236 final float deltaX = Math.abs(x - mInitialDownX);
237 final float deltaY = Math.abs(y - mInitialDownY);
Annie Chinc5b6e4f2016-12-05 13:34:14 -0800238 if (deltaX > slop && deltaX > deltaY) {
239 // Prevent the DragLayout from intercepting horizontal scrolls.
240 getParent().requestDisallowInterceptTouchEvent(true);
241 }
242 }
Justin Klaassen3a05c7e2016-03-04 12:40:02 -0800243 return mGestureDetector.onTouchEvent(event);
244 }
245 });
Hans Boehm14344ff2016-06-08 13:01:51 -0700246
Chenjie Yu3937b652016-06-01 23:14:26 -0700247 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
248 setupActionMode();
249 } else {
250 setupContextMenu();
251 }
Hans Boehm14344ff2016-06-08 13:01:51 -0700252
Hans Boehm84614952014-11-25 18:46:17 -0800253 setCursorVisible(false);
Christine Franksafe28bb2016-07-29 17:24:52 -0700254 setLongClickable(false);
Christine Franks6f6c24a2016-09-08 18:21:47 -0700255 setContentDescription(context.getString(R.string.desc_result));
Hans Boehm84614952014-11-25 18:46:17 -0800256 }
257
Hans Boehm8f051c32016-10-03 16:53:58 -0700258 void setEvaluator(Evaluator evaluator, long index) {
Hans Boehm84614952014-11-25 18:46:17 -0800259 mEvaluator = evaluator;
Hans Boehm8f051c32016-10-03 16:53:58 -0700260 mIndex = index;
Annie Chin7c586042016-11-18 15:57:37 -0800261 requestLayout();
Hans Boehm84614952014-11-25 18:46:17 -0800262 }
263
Hans Boehmcd72f7e2016-06-01 16:21:25 -0700264 // Compute maximum digit width the hard way.
265 private static float getMaxDigitWidth(TextPaint paint) {
266 // Compute the maximum advance width for each digit, thus accounting for between-character
267 // spaces. If we ever support other kinds of digits, we may have to avoid kerning effects
268 // that could reduce the advance width within this particular string.
269 final String allDigits = "0123456789";
270 final float[] widths = new float[allDigits.length()];
271 paint.getTextWidths(allDigits, widths);
272 float maxWidth = 0;
273 for (float x : widths) {
274 maxWidth = Math.max(x, maxWidth);
275 }
276 return maxWidth;
277 }
278
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700279 @Override
280 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Justin Klaassend06f51d2016-08-03 00:41:31 -0700281 if (!isLaidOut()) {
Annie Chin45e59972016-12-02 14:46:09 -0800282 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Justin Klaassend06f51d2016-08-03 00:41:31 -0700283 // Set a minimum height so scaled error messages won't affect our layout.
284 setMinimumHeight(getLineHeight() + getCompoundPaddingBottom()
285 + getCompoundPaddingTop());
286 }
287
Justin Klaassen44595162015-05-28 17:55:20 -0700288 final TextPaint paint = getPaint();
Hans Boehm80018c82015-08-02 16:59:07 -0700289 final Context context = getContext();
Hans Boehmcd72f7e2016-06-01 16:21:25 -0700290 final float newCharWidth = getMaxDigitWidth(paint);
Hans Boehm80018c82015-08-02 16:59:07 -0700291 // Digits are presumed to have no more than newCharWidth.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700292 // There are two instances when we know that the result is otherwise narrower than
293 // expected:
294 // 1. For standard scientific notation (our type 1), we know that we have a norrow decimal
295 // point and no (usually wide) ellipsis symbol. We allow one extra digit
296 // (SCI_NOTATION_EXTRA) to compensate, and consider that in determining available width.
297 // 2. If we are using digit grouping separators and a decimal point, we give ourselves
298 // a fractional extra space for those separators, the value of which depends on whether
299 // there is also an ellipsis.
300 //
301 // Maximum extra space we need in various cases:
302 // Type 1 scientific notation, assuming ellipsis, minus sign and E are wider than a digit:
303 // Two minus signs + "E" + "." - 3 digits.
304 // Type 2 scientific notation:
305 // Ellipsis + "E" + "-" - 3 digits.
306 // In the absence of scientific notation, we may need a little less space.
307 // We give ourselves a bit of extra credit towards comma insertion and give
308 // ourselves more if we have either
309 // No ellipsis, or
310 // A decimal separator.
311
312 // Calculate extra space we need to reserve, in addition to character count.
Hans Boehm80018c82015-08-02 16:59:07 -0700313 final float decimalSeparatorWidth = Layout.getDesiredWidth(
314 context.getString(R.string.dec_point), paint);
Hans Boehm24c91ed2016-06-30 18:53:44 -0700315 final float minusWidth = Layout.getDesiredWidth(context.getString(R.string.op_sub), paint);
316 final float minusExtraWidth = Math.max(minusWidth - newCharWidth, 0.0f);
317 final float ellipsisWidth = Layout.getDesiredWidth(KeyMaps.ELLIPSIS, paint);
318 final float ellipsisExtraWidth = Math.max(ellipsisWidth - newCharWidth, 0.0f);
319 final float expWidth = Layout.getDesiredWidth(KeyMaps.translateResult("e"), paint);
320 final float expExtraWidth = Math.max(expWidth - newCharWidth, 0.0f);
321 final float type1Extra = 2 * minusExtraWidth + expExtraWidth + decimalSeparatorWidth;
322 final float type2Extra = ellipsisExtraWidth + expExtraWidth + minusExtraWidth;
323 final float extraWidth = Math.max(type1Extra, type2Extra);
324 final int intExtraWidth = (int) Math.ceil(extraWidth) + 1 /* to cover rounding sins */;
Hans Boehm80018c82015-08-02 16:59:07 -0700325 final int newWidthConstraint = MeasureSpec.getSize(widthMeasureSpec)
Hans Boehm24c91ed2016-06-30 18:53:44 -0700326 - (getPaddingLeft() + getPaddingRight()) - intExtraWidth;
327
328 // Calculate other width constants we need to handle grouping separators.
329 final float groupingSeparatorW =
330 Layout.getDesiredWidth(KeyMaps.translateResult(","), paint);
331 // Credits in the absence of any scientific notation:
332 float noExponentCredit = extraWidth - Math.max(ellipsisExtraWidth, minusExtraWidth);
333 final float noEllipsisCredit = extraWidth - minusExtraWidth; // includes noExponentCredit.
334 final float decimalCredit = Math.max(newCharWidth - decimalSeparatorWidth, 0.0f);
335
336 mNoExponentCredit = noExponentCredit / newCharWidth;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700337 synchronized(mWidthLock) {
Hans Boehm013969e2015-04-13 20:29:47 -0700338 mWidthConstraint = newWidthConstraint;
339 mCharWidth = newCharWidth;
Hans Boehm24c91ed2016-06-30 18:53:44 -0700340 mNoEllipsisCredit = noEllipsisCredit / newCharWidth;
341 mDecimalCredit = decimalCredit / newCharWidth;
342 mGroupingSeparatorWidthRatio = groupingSeparatorW / newCharWidth;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700343 }
Hans Boehm14344ff2016-06-08 13:01:51 -0700344
345 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700346 }
347
Annie Chin06fd3cf2016-11-07 16:04:33 -0800348 @Override
349 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
350 super.onLayout(changed, left, top, right, bottom);
351
Hans Boehmbd01e4b2016-11-23 10:12:58 -0800352 if (mEvaluator != null && mEvaluationRequest != SHOULD_NOT_EVALUATE) {
Annie Chin06fd3cf2016-11-07 16:04:33 -0800353 final CalculatorExpr expr = mEvaluator.getExpr(mIndex);
354 if (expr != null && expr.hasInterestingOps()) {
Hans Boehmbd01e4b2016-11-23 10:12:58 -0800355 if (mEvaluationRequest == SHOULD_REQUIRE) {
356 mEvaluator.requireResult(mIndex, mEvaluationListener, this);
357 } else {
358 mEvaluator.evaluateAndNotify(mIndex, mEvaluationListener, this);
359 }
Annie Chin06fd3cf2016-11-07 16:04:33 -0800360 }
361 }
362 }
363
Hans Boehmbd01e4b2016-11-23 10:12:58 -0800364 /**
365 * Specify whether we should evaluate result on layout.
366 * @param should one of SHOULD_REQUIRE, SHOULD_EVALUATE, SHOULD_NOT_EVALUATE
367 */
368 public void setShouldEvaluateResult(@EvaluationRequest int request,
369 Evaluator.EvaluationListener listener) {
Hans Boehmd4959e82016-11-15 18:01:28 -0800370 mEvaluationListener = listener;
Hans Boehmbd01e4b2016-11-23 10:12:58 -0800371 mEvaluationRequest = request;
Annie Chinbc001882016-11-09 19:41:21 -0800372 }
373
Hans Boehm8f051c32016-10-03 16:53:58 -0700374 // From Evaluator.CharMetricsInfo.
375 @Override
Hans Boehm24c91ed2016-06-30 18:53:44 -0700376 public float separatorChars(String s, int len) {
377 int start = 0;
378 while (start < len && !Character.isDigit(s.charAt(start))) {
379 ++start;
380 }
381 // We assume the rest consists of digits, and for consistency with the rest
382 // of the code, we assume all digits have width mCharWidth.
383 final int nDigits = len - start;
384 // We currently insert a digit separator every three digits.
385 final int nSeparators = (nDigits - 1) / 3;
386 synchronized(mWidthLock) {
387 // Always return an upper bound, even in the presence of rounding errors.
388 return nSeparators * mGroupingSeparatorWidthRatio;
389 }
390 }
391
Hans Boehm8f051c32016-10-03 16:53:58 -0700392 // From Evaluator.CharMetricsInfo.
393 @Override
Hans Boehm24c91ed2016-06-30 18:53:44 -0700394 public float getNoEllipsisCredit() {
395 synchronized(mWidthLock) {
396 return mNoEllipsisCredit;
397 }
398 }
399
Hans Boehm8f051c32016-10-03 16:53:58 -0700400 // From Evaluator.CharMetricsInfo.
401 @Override
Hans Boehm24c91ed2016-06-30 18:53:44 -0700402 public float getDecimalCredit() {
403 synchronized(mWidthLock) {
404 return mDecimalCredit;
405 }
406 }
407
Hans Boehma0e45f32015-05-30 13:20:35 -0700408 // Return the length of the exponent representation for the given exponent, in
409 // characters.
410 private final int expLen(int exp) {
411 if (exp == 0) return 0;
Hans Boehm5e802f32015-06-22 17:18:52 -0700412 final int abs_exp_digits = (int) Math.ceil(Math.log10(Math.abs((double)exp))
413 + 0.0000000001d /* Round whole numbers to next integer */);
414 return abs_exp_digits + (exp >= 0 ? 1 : 2);
Hans Boehm61568a12015-05-18 18:25:41 -0700415 }
416
Hans Boehma0e45f32015-05-30 13:20:35 -0700417 /**
418 * Initiate display of a new result.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700419 * Only called from UI thread.
Hans Boehma0e45f32015-05-30 13:20:35 -0700420 * The parameters specify various properties of the result.
Hans Boehm8f051c32016-10-03 16:53:58 -0700421 * @param index Index of expression that was just evaluated. Currently ignored, since we only
422 * expect notification for the expression result being displayed.
Hans Boehma0e45f32015-05-30 13:20:35 -0700423 * @param initPrec Initial display precision computed by evaluator. (1 = tenths digit)
424 * @param msd Position of most significant digit. Offset from left of string.
425 Evaluator.INVALID_MSD if unknown.
426 * @param leastDigPos Position of least significant digit (1 = tenths digit)
427 * or Integer.MAX_VALUE.
428 * @param truncatedWholePart Result up to but not including decimal point.
429 Currently we only use the length.
430 */
Hans Boehm8f051c32016-10-03 16:53:58 -0700431 @Override
432 public void onEvaluate(long index, int initPrec, int msd, int leastDigPos,
433 String truncatedWholePart) {
Hans Boehma0e45f32015-05-30 13:20:35 -0700434 initPositions(initPrec, msd, leastDigPos, truncatedWholePart);
Annie Chin37c33b62016-11-22 14:46:28 -0800435
436 if (mStoreToMemoryRequested) {
437 mEvaluator.copyToMemory(index);
438 mStoreToMemoryRequested = false;
439 }
Hans Boehm84614952014-11-25 18:46:17 -0800440 redisplay();
441 }
442
Hans Boehma0e45f32015-05-30 13:20:35 -0700443 /**
Annie Chin37c33b62016-11-22 14:46:28 -0800444 * Store the result for this index if it is available.
445 * If it is unavailable, set mStoreToMemoryRequested to indicate that we should store
446 * when evaluation is complete.
447 */
448 public void onMemoryStore() {
449 if (mEvaluator.hasResult(mIndex)) {
450 mEvaluator.copyToMemory(mIndex);
451 } else {
452 mStoreToMemoryRequested = true;
453 mEvaluator.requireResult(mIndex, this /* listener */, this /* CharMetricsInfo */);
454 }
455 }
456
457 /**
Christine Franks1d99be12016-11-14 14:00:36 -0800458 * Add the result to the value currently in memory.
459 */
460 public void onMemoryAdd() {
Christine Franksff8e0d82016-11-23 12:28:26 -0800461 mEvaluator.addToMemory(mIndex);
Christine Franks1d99be12016-11-14 14:00:36 -0800462 }
463
464 /**
465 * Subtract the result from the value currently in memory.
466 */
467 public void onMemorySubtract() {
Christine Franksff8e0d82016-11-23 12:28:26 -0800468 mEvaluator.subtractFromMemory(mIndex);
Christine Franks1d99be12016-11-14 14:00:36 -0800469 }
470
471 /**
Hans Boehm5e802f32015-06-22 17:18:52 -0700472 * Set up scroll bounds (mMinPos, mMaxPos, etc.) and determine whether the result is
473 * scrollable, based on the supplied information about the result.
Hans Boehma0e45f32015-05-30 13:20:35 -0700474 * This is unfortunately complicated because we need to predict whether trailing digits
475 * will eventually be replaced by an exponent.
476 * Just appending the exponent during formatting would be simpler, but would produce
477 * jumpier results during transitions.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700478 * Only called from UI thread.
Hans Boehma0e45f32015-05-30 13:20:35 -0700479 */
Hans Boehm5e802f32015-06-22 17:18:52 -0700480 private void initPositions(int initPrecOffset, int msdIndex, int lsdOffset,
481 String truncatedWholePart) {
Hans Boehma0e45f32015-05-30 13:20:35 -0700482 int maxChars = getMaxChars();
Hans Boehm24c91ed2016-06-30 18:53:44 -0700483 mWholeLen = truncatedWholePart.length();
484 // Allow a tiny amount of slop for associativity/rounding differences in length
485 // calculation. If getPreferredPrec() decided it should fit, we want to make it fit, too.
486 // We reserved one extra pixel, so the extra length is OK.
487 final int nSeparatorChars = (int) Math.ceil(
488 separatorChars(truncatedWholePart, truncatedWholePart.length())
489 - getNoEllipsisCredit() - 0.0001f);
490 mWholePartFits = mWholeLen + nSeparatorChars <= maxChars;
Hans Boehma0e45f32015-05-30 13:20:35 -0700491 mLastPos = INVALID;
Hans Boehm5e802f32015-06-22 17:18:52 -0700492 mLsdOffset = lsdOffset;
Hans Boehm24c91ed2016-06-30 18:53:44 -0700493 mAppendExponent = false;
Hans Boehma0e45f32015-05-30 13:20:35 -0700494 // Prevent scrolling past initial position, which is calculated to show leading digits.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700495 mCurrentPos = mMinPos = (int) Math.round(initPrecOffset * mCharWidth);
Hans Boehm5e802f32015-06-22 17:18:52 -0700496 if (msdIndex == Evaluator.INVALID_MSD) {
Hans Boehma0e45f32015-05-30 13:20:35 -0700497 // Possible zero value
Hans Boehm5e802f32015-06-22 17:18:52 -0700498 if (lsdOffset == Integer.MIN_VALUE) {
Hans Boehma0e45f32015-05-30 13:20:35 -0700499 // Definite zero value.
500 mMaxPos = mMinPos;
Hans Boehm24c91ed2016-06-30 18:53:44 -0700501 mMaxCharOffset = (int) Math.round(mMaxPos/mCharWidth);
Hans Boehma0e45f32015-05-30 13:20:35 -0700502 mScrollable = false;
503 } else {
504 // May be very small nonzero value. Allow user to find out.
Hans Boehm5e802f32015-06-22 17:18:52 -0700505 mMaxPos = mMaxCharOffset = MAX_RIGHT_SCROLL;
Hans Boehm24c91ed2016-06-30 18:53:44 -0700506 mMinPos -= mCharWidth; // Allow for future minus sign.
Hans Boehma0e45f32015-05-30 13:20:35 -0700507 mScrollable = true;
508 }
509 return;
510 }
Hans Boehma0e45f32015-05-30 13:20:35 -0700511 int negative = truncatedWholePart.charAt(0) == '-' ? 1 : 0;
Hans Boehm65a99a42016-02-03 18:16:07 -0800512 if (msdIndex > mWholeLen && msdIndex <= mWholeLen + 3) {
Hans Boehm5e802f32015-06-22 17:18:52 -0700513 // Avoid tiny negative exponent; pretend msdIndex is just to the right of decimal point.
Hans Boehm65a99a42016-02-03 18:16:07 -0800514 msdIndex = mWholeLen - 1;
Hans Boehma0e45f32015-05-30 13:20:35 -0700515 }
Hans Boehm24c91ed2016-06-30 18:53:44 -0700516 // Set to position of leftmost significant digit relative to dec. point. Usually negative.
Hans Boehm65a99a42016-02-03 18:16:07 -0800517 int minCharOffset = msdIndex - mWholeLen;
Hans Boehm5e802f32015-06-22 17:18:52 -0700518 if (minCharOffset > -1 && minCharOffset < MAX_LEADING_ZEROES + 2) {
Hans Boehma0e45f32015-05-30 13:20:35 -0700519 // Small number of leading zeroes, avoid scientific notation.
Hans Boehm5e802f32015-06-22 17:18:52 -0700520 minCharOffset = -1;
Hans Boehma0e45f32015-05-30 13:20:35 -0700521 }
Hans Boehm5e802f32015-06-22 17:18:52 -0700522 if (lsdOffset < MAX_RIGHT_SCROLL) {
523 mMaxCharOffset = lsdOffset;
524 if (mMaxCharOffset < -1 && mMaxCharOffset > -(MAX_TRAILING_ZEROES + 2)) {
525 mMaxCharOffset = -1;
Hans Boehma0e45f32015-05-30 13:20:35 -0700526 }
Hans Boehm5e802f32015-06-22 17:18:52 -0700527 // lsdOffset is positive or negative, never 0.
528 int currentExpLen = 0; // Length of required standard scientific notation exponent.
529 if (mMaxCharOffset < -1) {
530 currentExpLen = expLen(-minCharOffset - 1);
531 } else if (minCharOffset > -1 || mMaxCharOffset >= maxChars) {
Hans Boehm24c91ed2016-06-30 18:53:44 -0700532 // Number is either entirely to the right of decimal point, or decimal point is
533 // not visible when scrolled to the right.
Hans Boehm5e802f32015-06-22 17:18:52 -0700534 currentExpLen = expLen(-minCharOffset);
Hans Boehma0e45f32015-05-30 13:20:35 -0700535 }
Hans Boehm24c91ed2016-06-30 18:53:44 -0700536 // Exponent length does not included added decimal point. But whenever we add a
537 // decimal point, we allow an extra character (SCI_NOTATION_EXTRA).
538 final int separatorLength = mWholePartFits && minCharOffset < -3 ? nSeparatorChars : 0;
539 mScrollable = (mMaxCharOffset + currentExpLen + separatorLength - minCharOffset
540 + negative >= maxChars);
541 // Now adjust mMaxCharOffset for any required exponent.
Hans Boehm5e802f32015-06-22 17:18:52 -0700542 int newMaxCharOffset;
543 if (currentExpLen > 0) {
544 if (mScrollable) {
545 // We'll use exponent corresponding to leastDigPos when scrolled to right.
546 newMaxCharOffset = mMaxCharOffset + expLen(-lsdOffset);
547 } else {
548 newMaxCharOffset = mMaxCharOffset + currentExpLen;
549 }
550 if (mMaxCharOffset <= -1 && newMaxCharOffset > -1) {
551 // Very unlikely; just drop exponent.
552 mMaxCharOffset = -1;
553 } else {
Hans Boehm24c91ed2016-06-30 18:53:44 -0700554 mMaxCharOffset = Math.min(newMaxCharOffset, MAX_RIGHT_SCROLL);
Hans Boehma0e45f32015-05-30 13:20:35 -0700555 }
Hans Boehm24c91ed2016-06-30 18:53:44 -0700556 mMaxPos = Math.min((int) Math.round(mMaxCharOffset * mCharWidth),
557 MAX_RIGHT_SCROLL);
558 } else if (!mWholePartFits && !mScrollable) {
559 // Corner case in which entire number fits, but not with grouping separators. We
560 // will use an exponent in un-scrolled position, which may hide digits. Scrolling
561 // by one character will remove the exponent and reveal the last digits. Note
562 // that in the forced scientific notation case, the exponent length is not
563 // factored into mMaxCharOffset, since we do not want such an increase to impact
564 // scrolling behavior. In the unscrollable case, we thus have to append the
565 // exponent at the end using the forcePrecision argument to formatResult, in order
566 // to ensure that we get the entire result.
567 mScrollable = (mMaxCharOffset + expLen(-minCharOffset - 1) - minCharOffset
568 + negative >= maxChars);
569 if (mScrollable) {
570 mMaxPos = (int) Math.ceil(mMinPos + mCharWidth);
571 // Single character scroll will remove exponent and show remaining piece.
572 } else {
573 mMaxPos = mMinPos;
574 mAppendExponent = true;
575 }
576 } else {
577 mMaxPos = Math.min((int) Math.round(mMaxCharOffset * mCharWidth),
578 MAX_RIGHT_SCROLL);
Hans Boehma0e45f32015-05-30 13:20:35 -0700579 }
Hans Boehma0e45f32015-05-30 13:20:35 -0700580 if (!mScrollable) {
581 // Position the number consistently with our assumptions to make sure it
582 // actually fits.
583 mCurrentPos = mMaxPos;
584 }
585 } else {
Hans Boehm5e802f32015-06-22 17:18:52 -0700586 mMaxPos = mMaxCharOffset = MAX_RIGHT_SCROLL;
Hans Boehma0e45f32015-05-30 13:20:35 -0700587 mScrollable = true;
588 }
589 }
590
Hans Boehm24c91ed2016-06-30 18:53:44 -0700591 /**
592 * Display error message indicated by resourceId.
593 * UI thread only.
594 */
Hans Boehm8f051c32016-10-03 16:53:58 -0700595 @Override
596 public void onError(long index, int resourceId) {
Annie Chin37c33b62016-11-22 14:46:28 -0800597 mStoreToMemoryRequested = false;
Annie Chin330419a2017-02-01 17:51:38 -0800598 mValid = false;
Christine Franksafe28bb2016-07-29 17:24:52 -0700599 setLongClickable(false);
Hans Boehm84614952014-11-25 18:46:17 -0800600 mScrollable = false;
Hans Boehm14344ff2016-06-08 13:01:51 -0700601 final String msg = getContext().getString(resourceId);
Hans Boehm14344ff2016-06-08 13:01:51 -0700602 final float measuredWidth = Layout.getDesiredWidth(msg, getPaint());
Hans Boehm24c91ed2016-06-30 18:53:44 -0700603 if (measuredWidth > mWidthConstraint) {
Hans Boehm14344ff2016-06-08 13:01:51 -0700604 // Multiply by .99 to avoid rounding effects.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700605 final float scaleFactor = 0.99f * mWidthConstraint / measuredWidth;
Hans Boehm14344ff2016-06-08 13:01:51 -0700606 final RelativeSizeSpan smallTextSpan = new RelativeSizeSpan(scaleFactor);
607 final SpannableString scaledMsg = new SpannableString(msg);
608 scaledMsg.setSpan(smallTextSpan, 0, msg.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
609 setText(scaledMsg);
610 } else {
611 setText(msg);
612 }
Hans Boehm84614952014-11-25 18:46:17 -0800613 }
614
Hans Boehm013969e2015-04-13 20:29:47 -0700615 private final int MAX_COPY_SIZE = 1000000;
616
Hans Boehma0e45f32015-05-30 13:20:35 -0700617 /*
618 * Return the most significant digit position in the given string or Evaluator.INVALID_MSD.
Hans Boehm3666e632015-07-27 18:33:12 -0700619 * Unlike Evaluator.getMsdIndexOf, we treat a final 1 as significant.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700620 * Pure function; callable from anywhere.
Hans Boehma0e45f32015-05-30 13:20:35 -0700621 */
Hans Boehm3666e632015-07-27 18:33:12 -0700622 public static int getNaiveMsdIndexOf(String s) {
Hans Boehm65a99a42016-02-03 18:16:07 -0800623 final int len = s.length();
Hans Boehma0e45f32015-05-30 13:20:35 -0700624 for (int i = 0; i < len; ++i) {
625 char c = s.charAt(i);
626 if (c != '-' && c != '.' && c != '0') {
627 return i;
628 }
629 }
630 return Evaluator.INVALID_MSD;
631 }
632
Hans Boehm24c91ed2016-06-30 18:53:44 -0700633 /**
634 * Format a result returned by Evaluator.getString() into a single line containing ellipses
635 * (if appropriate) and an exponent (if appropriate).
636 * We add two distinct kinds of exponents:
637 * (1) If the final result contains the leading digit we use standard scientific notation.
638 * (2) If not, we add an exponent corresponding to an interpretation of the final result as
639 * an integer.
640 * We add an ellipsis on the left if the result was truncated.
641 * We add ellipses and exponents in a way that leaves most digits in the position they
642 * would have been in had we not done so. This minimizes jumps as a result of scrolling.
643 * Result is NOT internationalized, uses "E" for exponent.
644 * Called only from UI thread; We sometimes omit locking for fields.
645 * @param precOffset The value that was passed to getString. Identifies the significance of
646 the rightmost digit. A value of 1 means the rightmost digits corresponds to tenths.
647 * @param maxDigs The maximum number of characters in the result
648 * @param truncated The in parameter was already truncated, beyond possibly removing the
649 minus sign.
650 * @param negative The in parameter represents a negative result. (Minus sign may be removed
651 without setting truncated.)
652 * @param lastDisplayedOffset If not null, we set lastDisplayedOffset[0] to the offset of
653 the last digit actually appearing in the display.
654 * @param forcePrecision If true, we make sure that the last displayed digit corresponds to
655 precOffset, and allow maxDigs to be exceeded in adding the exponent and commas.
656 * @param forceSciNotation Force scientific notation. May be set because we don't have
657 space for grouping separators, but whole number otherwise fits.
658 * @param insertCommas Insert commas (literally, not internationalized) as digit separators.
659 We only ever do this for the integral part of a number, and only when no
660 exponent is displayed in the initial position. The combination of which means
661 that we only do it when no exponent is displayed.
662 We insert commas in a way that does consider the width of the actual localized digit
663 separator. Commas count towards maxDigs as the appropriate fraction of a digit.
664 */
665 private String formatResult(String in, int precOffset, int maxDigs, boolean truncated,
666 boolean negative, int lastDisplayedOffset[], boolean forcePrecision,
667 boolean forceSciNotation, boolean insertCommas) {
Hans Boehm5e802f32015-06-22 17:18:52 -0700668 final int minusSpace = negative ? 1 : 0;
Hans Boehm3666e632015-07-27 18:33:12 -0700669 final int msdIndex = truncated ? -1 : getNaiveMsdIndexOf(in); // INVALID_MSD is OK.
Hans Boehm5e802f32015-06-22 17:18:52 -0700670 String result = in;
Hans Boehm24c91ed2016-06-30 18:53:44 -0700671 boolean needEllipsis = false;
Hans Boehm73ecff22015-09-03 16:04:50 -0700672 if (truncated || (negative && result.charAt(0) != '-')) {
Hans Boehm24c91ed2016-06-30 18:53:44 -0700673 needEllipsis = true;
Hans Boehm73ecff22015-09-03 16:04:50 -0700674 result = KeyMaps.ELLIPSIS + result.substring(1, result.length());
675 // Ellipsis may be removed again in the type(1) scientific notation case.
676 }
677 final int decIndex = result.indexOf('.');
Hans Boehm65a99a42016-02-03 18:16:07 -0800678 if (lastDisplayedOffset != null) {
679 lastDisplayedOffset[0] = precOffset;
680 }
Hans Boehm24c91ed2016-06-30 18:53:44 -0700681 if (forceSciNotation || (decIndex == -1 || msdIndex != Evaluator.INVALID_MSD
Hans Boehm5e802f32015-06-22 17:18:52 -0700682 && msdIndex - decIndex > MAX_LEADING_ZEROES + 1) && precOffset != -1) {
Hans Boehm24c91ed2016-06-30 18:53:44 -0700683 // Either:
684 // 1) No decimal point displayed, and it's not just to the right of the last digit, or
685 // 2) we are at the front of a number whos integral part is too large to allow
686 // comma insertion, or
687 // 3) we should suppress leading zeroes.
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700688 // Add an exponent to let the user track which digits are currently displayed.
Hans Boehm5e802f32015-06-22 17:18:52 -0700689 // Start with type (2) exponent if we dropped no digits. -1 accounts for decimal point.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700690 // We currently never show digit separators together with an exponent.
Hans Boehm5e802f32015-06-22 17:18:52 -0700691 final int initExponent = precOffset > 0 ? -precOffset : -precOffset - 1;
692 int exponent = initExponent;
Hans Boehm08e8f322015-04-21 13:18:38 -0700693 boolean hasPoint = false;
Hans Boehm5e802f32015-06-22 17:18:52 -0700694 if (!truncated && msdIndex < maxDigs - 1
695 && result.length() - msdIndex + 1 + minusSpace
696 <= maxDigs + SCI_NOTATION_EXTRA) {
697 // Type (1) exponent computation and transformation:
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700698 // Leading digit is in display window. Use standard calculator scientific notation
699 // with one digit to the left of the decimal point. Insert decimal point and
700 // delete leading zeroes.
Hans Boehma0e45f32015-05-30 13:20:35 -0700701 // We try to keep leading digits roughly in position, and never
Hans Boehmf6dae112015-06-18 17:57:50 -0700702 // lengthen the result by more than SCI_NOTATION_EXTRA.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700703 if (decIndex > msdIndex) {
704 // In the forceSciNotation, we can have a decimal point in the relevant digit
705 // range. Remove it.
706 result = result.substring(0, decIndex)
707 + result.substring(decIndex + 1, result.length());
708 // msdIndex and precOffset unaffected.
709 }
Hans Boehm5e802f32015-06-22 17:18:52 -0700710 final int resLen = result.length();
711 String fraction = result.substring(msdIndex + 1, resLen);
712 result = (negative ? "-" : "") + result.substring(msdIndex, msdIndex + 1)
713 + "." + fraction;
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700714 // Original exp was correct for decimal point at right of fraction.
715 // Adjust by length of fraction.
Hans Boehm5e802f32015-06-22 17:18:52 -0700716 exponent = initExponent + resLen - msdIndex - 1;
Hans Boehm08e8f322015-04-21 13:18:38 -0700717 hasPoint = true;
718 }
Hans Boehm73ecff22015-09-03 16:04:50 -0700719 // Exponent can't be zero.
720 // Actually add the exponent of either type:
721 if (!forcePrecision) {
722 int dropDigits; // Digits to drop to make room for exponent.
723 if (hasPoint) {
724 // Type (1) exponent.
725 // Drop digits even if there is room. Otherwise the scrolling gets jumpy.
726 dropDigits = expLen(exponent);
727 if (dropDigits >= result.length() - 1) {
728 // Jumpy is better than no mantissa. Probably impossible anyway.
729 dropDigits = Math.max(result.length() - 2, 0);
Hans Boehma0e45f32015-05-30 13:20:35 -0700730 }
Hans Boehm73ecff22015-09-03 16:04:50 -0700731 } else {
732 // Type (2) exponent.
733 // Exponent depends on the number of digits we drop, which depends on
734 // exponent ...
735 for (dropDigits = 2; expLen(initExponent + dropDigits) > dropDigits;
736 ++dropDigits) {}
737 exponent = initExponent + dropDigits;
738 if (precOffset - dropDigits > mLsdOffset) {
739 // This can happen if e.g. result = 10^40 + 10^10
740 // It turns out we would otherwise display ...10e9 because it takes
741 // the same amount of space as ...1e10 but shows one more digit.
742 // But we don't want to display a trailing zero, even if it's free.
743 ++dropDigits;
744 ++exponent;
745 }
Hans Boehm08e8f322015-04-21 13:18:38 -0700746 }
Hans Boehm64751002017-02-02 17:09:18 -0800747 if (dropDigits >= result.length() - 1) {
748 // Display too small to show meaningful result.
749 return KeyMaps.ELLIPSIS + "E" + KeyMaps.ELLIPSIS;
750 }
Hans Boehm73ecff22015-09-03 16:04:50 -0700751 result = result.substring(0, result.length() - dropDigits);
Hans Boehm65a99a42016-02-03 18:16:07 -0800752 if (lastDisplayedOffset != null) {
753 lastDisplayedOffset[0] -= dropDigits;
754 }
Hans Boehm73ecff22015-09-03 16:04:50 -0700755 }
756 result = result + "E" + Integer.toString(exponent);
Hans Boehm24c91ed2016-06-30 18:53:44 -0700757 } else if (insertCommas) {
758 // Add commas to the whole number section, and then truncate on left to fit,
759 // counting commas as a fractional digit.
760 final int wholeStart = needEllipsis ? 1 : 0;
761 int orig_length = result.length();
762 final float nCommaChars;
763 if (decIndex != -1) {
764 nCommaChars = separatorChars(result, decIndex);
765 result = StringUtils.addCommas(result, wholeStart, decIndex)
766 + result.substring(decIndex, orig_length);
767 } else {
768 nCommaChars = separatorChars(result, orig_length);
769 result = StringUtils.addCommas(result, wholeStart, orig_length);
770 }
771 if (needEllipsis) {
772 orig_length -= 1; // Exclude ellipsis.
773 }
774 final float len = orig_length + nCommaChars;
775 int deletedChars = 0;
776 final float ellipsisCredit = getNoEllipsisCredit();
777 final float decimalCredit = getDecimalCredit();
778 final float effectiveLen = len - (decIndex == -1 ? 0 : getDecimalCredit());
779 final float ellipsisAdjustment =
780 needEllipsis ? mNoExponentCredit : getNoEllipsisCredit();
781 // As above, we allow for a tiny amount of extra length here, for consistency with
782 // getPreferredPrec().
783 if (effectiveLen - ellipsisAdjustment > (float) (maxDigs - wholeStart) + 0.0001f
784 && !forcePrecision) {
785 float deletedWidth = 0.0f;
786 while (effectiveLen - mNoExponentCredit - deletedWidth
787 > (float) (maxDigs - 1 /* for ellipsis */)) {
788 if (result.charAt(deletedChars) == ',') {
789 deletedWidth += mGroupingSeparatorWidthRatio;
790 } else {
791 deletedWidth += 1.0f;
792 }
793 deletedChars++;
794 }
795 }
796 if (deletedChars > 0) {
797 result = KeyMaps.ELLIPSIS + result.substring(deletedChars, result.length());
798 } else if (needEllipsis) {
799 result = KeyMaps.ELLIPSIS + result;
800 }
Hans Boehm5e802f32015-06-22 17:18:52 -0700801 }
802 return result;
Hans Boehm08e8f322015-04-21 13:18:38 -0700803 }
804
Hans Boehmf6dae112015-06-18 17:57:50 -0700805 /**
806 * Get formatted, but not internationalized, result from mEvaluator.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700807 * @param precOffset requested position (1 = tenths) of last included digit
808 * @param maxSize maximum number of characters (more or less) in result
809 * @param lastDisplayedOffset zeroth entry is set to actual offset of last included digit,
Hans Boehm65a99a42016-02-03 18:16:07 -0800810 * after adjusting for exponent, etc. May be null.
Hans Boehmf6dae112015-06-18 17:57:50 -0700811 * @param forcePrecision Ensure that last included digit is at pos, at the expense
812 * of treating maxSize as a soft limit.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700813 * @param forceSciNotation Force scientific notation, even if not required by maxSize.
814 * @param insertCommas Insert commas as digit separators.
Hans Boehmf6dae112015-06-18 17:57:50 -0700815 */
Hans Boehm5e802f32015-06-22 17:18:52 -0700816 private String getFormattedResult(int precOffset, int maxSize, int lastDisplayedOffset[],
Hans Boehm24c91ed2016-06-30 18:53:44 -0700817 boolean forcePrecision, boolean forceSciNotation, boolean insertCommas) {
Hans Boehm08e8f322015-04-21 13:18:38 -0700818 final boolean truncated[] = new boolean[1];
819 final boolean negative[] = new boolean[1];
Hans Boehm5e802f32015-06-22 17:18:52 -0700820 final int requestedPrecOffset[] = {precOffset};
Hans Boehm8f051c32016-10-03 16:53:58 -0700821 final String rawResult = mEvaluator.getString(mIndex, requestedPrecOffset, mMaxCharOffset,
822 maxSize, truncated, negative, this);
Hans Boehm5e802f32015-06-22 17:18:52 -0700823 return formatResult(rawResult, requestedPrecOffset[0], maxSize, truncated[0], negative[0],
Hans Boehm24c91ed2016-06-30 18:53:44 -0700824 lastDisplayedOffset, forcePrecision, forceSciNotation, insertCommas);
Hans Boehm08e8f322015-04-21 13:18:38 -0700825 }
826
Hans Boehm65a99a42016-02-03 18:16:07 -0800827 /**
828 * Return entire result (within reason) up to current displayed precision.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700829 * @param withSeparators Add digit separators
Hans Boehm65a99a42016-02-03 18:16:07 -0800830 */
Hans Boehm24c91ed2016-06-30 18:53:44 -0700831 public String getFullText(boolean withSeparators) {
Hans Boehm760a9dc2015-04-20 10:27:12 -0700832 if (!mValid) return "";
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700833 if (!mScrollable) return getText().toString();
Hans Boehm5e802f32015-06-22 17:18:52 -0700834 return KeyMaps.translateResult(getFormattedResult(mLastDisplayedOffset, MAX_COPY_SIZE,
Hans Boehm24c91ed2016-06-30 18:53:44 -0700835 null, true /* forcePrecision */, false /* forceSciNotation */, withSeparators));
Hans Boehm84614952014-11-25 18:46:17 -0800836 }
837
Hans Boehm24c91ed2016-06-30 18:53:44 -0700838 /**
839 * Did the above produce a correct result?
840 * UI thread only.
841 */
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700842 public boolean fullTextIsExact() {
Hans Boehm3465c782016-12-12 17:28:10 -0800843 return !mScrollable || (getCharOffset(mMaxPos) == getCharOffset(mCurrentPos)
Hans Boehm24c91ed2016-06-30 18:53:44 -0700844 && mMaxCharOffset != MAX_RIGHT_SCROLL);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700845 }
846
Hans Boehm61568a12015-05-18 18:25:41 -0700847 /**
Hans Boehm65a99a42016-02-03 18:16:07 -0800848 * Get entire result up to current displayed precision, or up to MAX_COPY_EXTRA additional
849 * digits, if it will lead to an exact result.
850 */
851 public String getFullCopyText() {
852 if (!mValid
853 || mLsdOffset == Integer.MAX_VALUE
854 || fullTextIsExact()
855 || mWholeLen > MAX_RECOMPUTE_DIGITS
856 || mWholeLen + mLsdOffset > MAX_RECOMPUTE_DIGITS
857 || mLsdOffset - mLastDisplayedOffset > MAX_COPY_EXTRA) {
Hans Boehm24c91ed2016-06-30 18:53:44 -0700858 return getFullText(false /* withSeparators */);
Hans Boehm65a99a42016-02-03 18:16:07 -0800859 }
860 // It's reasonable to compute and copy the exact result instead.
Hans Boehm3465c782016-12-12 17:28:10 -0800861 int fractionLsdOffset = Math.max(0, mLsdOffset);
862 String rawResult = mEvaluator.getResult(mIndex).toStringTruncated(fractionLsdOffset);
863 if (mLsdOffset <= -1) {
864 // Result has trailing decimal point. Remove it.
865 rawResult = rawResult.substring(0, rawResult.length() - 1);
866 fractionLsdOffset = -1;
867 }
868 final String formattedResult = formatResult(rawResult, fractionLsdOffset, MAX_COPY_SIZE,
Hans Boehm24c91ed2016-06-30 18:53:44 -0700869 false, rawResult.charAt(0) == '-', null, true /* forcePrecision */,
870 false /* forceSciNotation */, false /* insertCommas */);
Hans Boehm65a99a42016-02-03 18:16:07 -0800871 return KeyMaps.translateResult(formattedResult);
872 }
873
874 /**
Hans Boehm61568a12015-05-18 18:25:41 -0700875 * Return the maximum number of characters that will fit in the result display.
Hans Boehm8f051c32016-10-03 16:53:58 -0700876 * May be called asynchronously from non-UI thread. From Evaluator.CharMetricsInfo.
Hans Boehmd4959e82016-11-15 18:01:28 -0800877 * Returns zero if measurement hasn't completed.
Hans Boehm61568a12015-05-18 18:25:41 -0700878 */
Hans Boehm8f051c32016-10-03 16:53:58 -0700879 @Override
880 public int getMaxChars() {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700881 int result;
882 synchronized(mWidthLock) {
Hans Boehmd4959e82016-11-15 18:01:28 -0800883 return (int) Math.floor(mWidthConstraint / mCharWidth);
Hans Boehm84614952014-11-25 18:46:17 -0800884 }
885 }
886
Hans Boehm61568a12015-05-18 18:25:41 -0700887 /**
Justin Klaassen44595162015-05-28 17:55:20 -0700888 * @return {@code true} if the currently displayed result is scrollable
Hans Boehm61568a12015-05-18 18:25:41 -0700889 */
Justin Klaassen44595162015-05-28 17:55:20 -0700890 public boolean isScrollable() {
891 return mScrollable;
Hans Boehm61568a12015-05-18 18:25:41 -0700892 }
893
Hans Boehm24c91ed2016-06-30 18:53:44 -0700894 /**
895 * Map pixel position to digit offset.
896 * UI thread only.
897 */
898 int getCharOffset(int pos) {
899 return (int) Math.round(pos / mCharWidth); // Lock not needed.
Hans Boehm013969e2015-04-13 20:29:47 -0700900 }
901
Hans Boehm84614952014-11-25 18:46:17 -0800902 void clear() {
Hans Boehm760a9dc2015-04-20 10:27:12 -0700903 mValid = false;
Hans Boehm1176f232015-05-11 16:26:03 -0700904 mScrollable = false;
Hans Boehm84614952014-11-25 18:46:17 -0800905 setText("");
Christine Franksafe28bb2016-07-29 17:24:52 -0700906 setLongClickable(false);
Hans Boehm84614952014-11-25 18:46:17 -0800907 }
908
Hans Boehm8f051c32016-10-03 16:53:58 -0700909 @Override
910 public void onCancelled(long index) {
911 clear();
Annie Chin37c33b62016-11-22 14:46:28 -0800912 mStoreToMemoryRequested = false;
Hans Boehm8f051c32016-10-03 16:53:58 -0700913 }
914
Hans Boehm24c91ed2016-06-30 18:53:44 -0700915 /**
916 * Refresh display.
Hans Boehm8f051c32016-10-03 16:53:58 -0700917 * Only called in UI thread. Index argument is currently ignored.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700918 */
Hans Boehm8f051c32016-10-03 16:53:58 -0700919 @Override
920 public void onReevaluate(long index) {
921 redisplay();
922 }
923
924 public void redisplay() {
Hans Boehm64751002017-02-02 17:09:18 -0800925 int maxChars = getMaxChars();
926 if (maxChars < 4) {
927 // Display currently too small to display a reasonable result. Punt to avoid crash.
928 return;
929 }
Christine Franks6f6c24a2016-09-08 18:21:47 -0700930 if (mScroller.isFinished() && length() > 0) {
Christine Franksd21205c2016-08-04 10:06:15 -0700931 setAccessibilityLiveRegion(ACCESSIBILITY_LIVE_REGION_POLITE);
932 }
Hans Boehm24c91ed2016-06-30 18:53:44 -0700933 int currentCharOffset = getCharOffset(mCurrentPos);
Hans Boehm5e802f32015-06-22 17:18:52 -0700934 int lastDisplayedOffset[] = new int[1];
Hans Boehm24c91ed2016-06-30 18:53:44 -0700935 String result = getFormattedResult(currentCharOffset, maxChars, lastDisplayedOffset,
936 mAppendExponent /* forcePrecision; preserve entire result */,
937 !mWholePartFits
938 && currentCharOffset == getCharOffset(mMinPos) /* forceSciNotation */,
939 mWholePartFits /* insertCommas */ );
Hans Boehm0b9806f2015-06-29 16:07:15 -0700940 int expIndex = result.indexOf('E');
Hans Boehm013969e2015-04-13 20:29:47 -0700941 result = KeyMaps.translateResult(result);
Hans Boehm5e802f32015-06-22 17:18:52 -0700942 if (expIndex > 0 && result.indexOf('.') == -1) {
Hans Boehm84614952014-11-25 18:46:17 -0800943 // Gray out exponent if used as position indicator
944 SpannableString formattedResult = new SpannableString(result);
Hans Boehm5e802f32015-06-22 17:18:52 -0700945 formattedResult.setSpan(mExponentColorSpan, expIndex, result.length(),
Hans Boehm84614952014-11-25 18:46:17 -0800946 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
947 setText(formattedResult);
948 } else {
949 setText(result);
950 }
Hans Boehm5e802f32015-06-22 17:18:52 -0700951 mLastDisplayedOffset = lastDisplayedOffset[0];
Hans Boehm760a9dc2015-04-20 10:27:12 -0700952 mValid = true;
Christine Franksafe28bb2016-07-29 17:24:52 -0700953 setLongClickable(true);
Hans Boehm84614952014-11-25 18:46:17 -0800954 }
955
956 @Override
Christine Franks6f6c24a2016-09-08 18:21:47 -0700957 protected void onTextChanged(java.lang.CharSequence text, int start, int lengthBefore,
958 int lengthAfter) {
959 super.onTextChanged(text, start, lengthBefore, lengthAfter);
960
961 if (!mScrollable || mScroller.isFinished()) {
962 if (lengthBefore == 0 && lengthAfter > 0) {
963 setAccessibilityLiveRegion(ACCESSIBILITY_LIVE_REGION_POLITE);
964 setContentDescription(null);
965 } else if (lengthBefore > 0 && lengthAfter == 0) {
966 setAccessibilityLiveRegion(ACCESSIBILITY_LIVE_REGION_NONE);
967 setContentDescription(getContext().getString(R.string.desc_result));
968 }
969 }
970 }
971
972 @Override
Hans Boehm84614952014-11-25 18:46:17 -0800973 public void computeScroll() {
Christine Franks6f6c24a2016-09-08 18:21:47 -0700974 if (!mScrollable) {
975 return;
976 }
977
Hans Boehm84614952014-11-25 18:46:17 -0800978 if (mScroller.computeScrollOffset()) {
979 mCurrentPos = mScroller.getCurrX();
Hans Boehm24c91ed2016-06-30 18:53:44 -0700980 if (getCharOffset(mCurrentPos) != getCharOffset(mLastPos)) {
Hans Boehm84614952014-11-25 18:46:17 -0800981 mLastPos = mCurrentPos;
982 redisplay();
983 }
Christine Franks6f6c24a2016-09-08 18:21:47 -0700984 }
985
986 if (!mScroller.isFinished()) {
Justin Klaassen44595162015-05-28 17:55:20 -0700987 postInvalidateOnAnimation();
Christine Franksd21205c2016-08-04 10:06:15 -0700988 setAccessibilityLiveRegion(ACCESSIBILITY_LIVE_REGION_NONE);
Christine Franks6f6c24a2016-09-08 18:21:47 -0700989 } else if (length() > 0){
Christine Franksd21205c2016-08-04 10:06:15 -0700990 setAccessibilityLiveRegion(ACCESSIBILITY_LIVE_REGION_POLITE);
Hans Boehm84614952014-11-25 18:46:17 -0800991 }
992 }
993
Chenjie Yu3937b652016-06-01 23:14:26 -0700994 /**
Christine Franks1d99be12016-11-14 14:00:36 -0800995 * Use ActionMode for copy/memory support on M and higher.
Chenjie Yu3937b652016-06-01 23:14:26 -0700996 */
997 @TargetApi(Build.VERSION_CODES.M)
998 private void setupActionMode() {
999 mCopyActionModeCallback = new ActionMode.Callback2() {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001000
Chenjie Yu3937b652016-06-01 23:14:26 -07001001 @Override
1002 public boolean onCreateActionMode(ActionMode mode, Menu menu) {
1003 final MenuInflater inflater = mode.getMenuInflater();
Christine Franks7485df52016-12-01 13:18:45 -08001004 return createContextMenu(inflater, menu);
Chenjie Yu3937b652016-06-01 23:14:26 -07001005 }
Hans Boehm7f83e362015-06-10 15:41:04 -07001006
Chenjie Yu3937b652016-06-01 23:14:26 -07001007 @Override
1008 public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
1009 return false; // Return false if nothing is done
1010 }
Hans Boehm7f83e362015-06-10 15:41:04 -07001011
Chenjie Yu3937b652016-06-01 23:14:26 -07001012 @Override
1013 public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
1014 if (onMenuItemClick(item)) {
Hans Boehm65a99a42016-02-03 18:16:07 -08001015 mode.finish();
1016 return true;
Chenjie Yu3937b652016-06-01 23:14:26 -07001017 } else {
1018 return false;
Hans Boehm65a99a42016-02-03 18:16:07 -08001019 }
Chenjie Yu3937b652016-06-01 23:14:26 -07001020 }
1021
1022 @Override
1023 public void onDestroyActionMode(ActionMode mode) {
1024 unhighlightResult();
1025 mActionMode = null;
1026 }
1027
1028 @Override
1029 public void onGetContentRect(ActionMode mode, View view, Rect outRect) {
1030 super.onGetContentRect(mode, view, outRect);
1031
1032 outRect.left += view.getPaddingLeft();
1033 outRect.top += view.getPaddingTop();
1034 outRect.right -= view.getPaddingRight();
1035 outRect.bottom -= view.getPaddingBottom();
1036 final int width = (int) Layout.getDesiredWidth(getText(), getPaint());
1037 if (width < outRect.width()) {
1038 outRect.left = outRect.right - width;
1039 }
1040
1041 if (!BuildCompat.isAtLeastN()) {
1042 // The CAB (prior to N) only takes the translation of a view into account, so
1043 // if a scale is applied to the view then the offset outRect will end up being
1044 // positioned incorrectly. We workaround that limitation by manually applying
1045 // the scale to the outRect, which the CAB will then offset to the correct
1046 // position.
1047 final float scaleX = view.getScaleX();
1048 final float scaleY = view.getScaleY();
1049 outRect.left *= scaleX;
1050 outRect.right *= scaleX;
1051 outRect.top *= scaleY;
1052 outRect.bottom *= scaleY;
1053 }
1054 }
1055 };
1056 setOnLongClickListener(new View.OnLongClickListener() {
1057 @Override
1058 public boolean onLongClick(View v) {
1059 if (mValid) {
1060 mActionMode = startActionMode(mCopyActionModeCallback,
1061 ActionMode.TYPE_FLOATING);
1062 return true;
1063 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001064 return false;
1065 }
Chenjie Yu3937b652016-06-01 23:14:26 -07001066 });
1067 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001068
Chenjie Yu3937b652016-06-01 23:14:26 -07001069 /**
Christine Franks1d99be12016-11-14 14:00:36 -08001070 * Use ContextMenu for copy/memory support on L and lower.
Chenjie Yu3937b652016-06-01 23:14:26 -07001071 */
1072 private void setupContextMenu() {
1073 setOnCreateContextMenuListener(new OnCreateContextMenuListener() {
1074 @Override
1075 public void onCreateContextMenu(ContextMenu contextMenu, View view,
1076 ContextMenu.ContextMenuInfo contextMenuInfo) {
1077 final MenuInflater inflater = new MenuInflater(getContext());
Christine Franks7485df52016-12-01 13:18:45 -08001078 createContextMenu(inflater, contextMenu);
Chenjie Yu3937b652016-06-01 23:14:26 -07001079 mContextMenu = contextMenu;
Hans Boehm9c160b42016-12-02 11:55:12 -08001080 for (int i = 0; i < contextMenu.size(); i ++) {
Chenjie Yu3937b652016-06-01 23:14:26 -07001081 contextMenu.getItem(i).setOnMenuItemClickListener(CalculatorResult.this);
1082 }
Hans Boehm7f83e362015-06-10 15:41:04 -07001083 }
Chenjie Yu3937b652016-06-01 23:14:26 -07001084 });
1085 setOnLongClickListener(new View.OnLongClickListener() {
1086 @Override
1087 public boolean onLongClick(View v) {
1088 if (mValid) {
1089 return showContextMenu();
1090 }
1091 return false;
Justin Klaassenf1b61f42016-04-27 16:00:11 -07001092 }
Chenjie Yu3937b652016-06-01 23:14:26 -07001093 });
1094 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001095
Christine Franks7485df52016-12-01 13:18:45 -08001096 private boolean createContextMenu(MenuInflater inflater, Menu menu) {
Christine Franks1d99be12016-11-14 14:00:36 -08001097 inflater.inflate(R.menu.menu_result, menu);
1098 final boolean displayMemory = mEvaluator.getMemoryIndex() != 0;
1099 final MenuItem memoryAddItem = menu.findItem(R.id.memory_add);
1100 final MenuItem memorySubtractItem = menu.findItem(R.id.memory_subtract);
1101 memoryAddItem.setEnabled(displayMemory);
1102 memorySubtractItem.setEnabled(displayMemory);
Chenjie Yu3937b652016-06-01 23:14:26 -07001103 highlightResult();
1104 return true;
1105 }
1106
1107 public boolean stopActionModeOrContextMenu() {
Hans Boehm1176f232015-05-11 16:26:03 -07001108 if (mActionMode != null) {
1109 mActionMode.finish();
1110 return true;
1111 }
Chenjie Yu3937b652016-06-01 23:14:26 -07001112 if (mContextMenu != null) {
1113 unhighlightResult();
1114 mContextMenu.close();
1115 return true;
1116 }
Hans Boehm1176f232015-05-11 16:26:03 -07001117 return false;
1118 }
1119
Chenjie Yu3937b652016-06-01 23:14:26 -07001120 private void highlightResult() {
1121 final Spannable text = (Spannable) getText();
1122 text.setSpan(mHighlightSpan, 0, text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
1123 }
1124
1125 private void unhighlightResult() {
1126 final Spannable text = (Spannable) getText();
1127 text.removeSpan(mHighlightSpan);
1128 }
1129
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001130 private void setPrimaryClip(ClipData clip) {
1131 ClipboardManager clipboard = (ClipboardManager) getContext().
Hans Boehmc01cd7f2015-05-12 18:32:19 -07001132 getSystemService(Context.CLIPBOARD_SERVICE);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001133 clipboard.setPrimaryClip(clip);
1134 }
1135
1136 private void copyContent() {
Hans Boehm65a99a42016-02-03 18:16:07 -08001137 final CharSequence text = getFullCopyText();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001138 ClipboardManager clipboard =
Hans Boehmc01cd7f2015-05-12 18:32:19 -07001139 (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
1140 // We include a tag URI, to allow us to recognize our own results and handle them
1141 // specially.
Hans Boehm8f051c32016-10-03 16:53:58 -07001142 ClipData.Item newItem = new ClipData.Item(text, null, mEvaluator.capture(mIndex));
Hans Boehmc01cd7f2015-05-12 18:32:19 -07001143 String[] mimeTypes = new String[] {ClipDescription.MIMETYPE_TEXT_PLAIN};
1144 ClipData cd = new ClipData("calculator result", mimeTypes, newItem);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001145 clipboard.setPrimaryClip(cd);
Hans Boehmc01cd7f2015-05-12 18:32:19 -07001146 Toast.makeText(getContext(), R.string.text_copied_toast, Toast.LENGTH_SHORT).show();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001147 }
1148
Chenjie Yu3937b652016-06-01 23:14:26 -07001149 @Override
1150 public boolean onMenuItemClick(MenuItem item) {
1151 switch (item.getItemId()) {
Christine Franks1d99be12016-11-14 14:00:36 -08001152 case R.id.memory_add:
1153 onMemoryAdd();
1154 return true;
1155 case R.id.memory_subtract:
1156 onMemorySubtract();
1157 return true;
1158 case R.id.memory_store:
1159 onMemoryStore();
1160 return true;
Chenjie Yu3937b652016-06-01 23:14:26 -07001161 case R.id.menu_copy:
Hans Boehm8f051c32016-10-03 16:53:58 -07001162 if (mEvaluator.evaluationInProgress(mIndex)) {
Chenjie Yu3937b652016-06-01 23:14:26 -07001163 // Refuse to copy placeholder characters.
1164 return false;
1165 } else {
1166 copyContent();
1167 unhighlightResult();
1168 return true;
1169 }
1170 default:
1171 return false;
1172 }
1173 }
Christine Franks7485df52016-12-01 13:18:45 -08001174
1175 @Override
1176 protected void onDetachedFromWindow() {
1177 stopActionModeOrContextMenu();
1178 super.onDetachedFromWindow();
1179 }
Hans Boehmbd01e4b2016-11-23 10:12:58 -08001180}