blob: 6934d39a2a92f4a2c90fca41fd783e46136b0073 [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;
Hans Boehmbd01e4b2016-11-23 10:12:58 -080026import android.support.annotation.IntDef;
Chenjie Yu3937b652016-06-01 23:14:26 -070027import android.support.v4.content.ContextCompat;
Justin Klaassenf1b61f42016-04-27 16:00:11 -070028import android.support.v4.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;
Hans Boehmc01cd7f2015-05-12 18:32:19 -070067 // The result holds something valid; either a a number or an error
68 // message.
Hans Boehm5e802f32015-06-22 17:18:52 -070069 // A suffix of "Pos" denotes a pixel offset. Zero represents a scroll position
70 // in which the decimal point is just barely visible on the right of the display.
Hans Boehmc01cd7f2015-05-12 18:32:19 -070071 private int mCurrentPos;// Position of right of display relative to decimal point, in pixels.
72 // Large positive values mean the decimal point is scrolled off the
73 // left of the display. Zero means decimal point is barely displayed
74 // on the right.
Hans Boehm61568a12015-05-18 18:25:41 -070075 private int mLastPos; // Position already reflected in display. Pixels.
Hans Boehm65a99a42016-02-03 18:16:07 -080076 private int mMinPos; // Minimum position to avoid unnecessary blanks on the left. Pixels.
Hans Boehm61568a12015-05-18 18:25:41 -070077 private int mMaxPos; // Maximum position before we start displaying the infinite
78 // sequence of trailing zeroes on the right. Pixels.
Hans Boehm65a99a42016-02-03 18:16:07 -080079 private int mWholeLen; // Length of the whole part of current result.
Hans Boehm5e802f32015-06-22 17:18:52 -070080 // In the following, we use a suffix of Offset to denote a character position in a numeric
81 // string relative to the decimal point. Positive is to the right and negative is to
82 // the left. 1 = tenths position, -1 = units. Integer.MAX_VALUE is sometimes used
83 // for the offset of the last digit in an a nonterminating decimal expansion.
84 // We use the suffix "Index" to denote a zero-based index into a string representing a
85 // result.
Hans Boehm5e802f32015-06-22 17:18:52 -070086 private int mMaxCharOffset; // Character offset from decimal point of rightmost digit
Hans Boehm24c91ed2016-06-30 18:53:44 -070087 // that should be displayed, plus the length of any exponent
88 // needed to display that digit.
89 // Limited to MAX_RIGHT_SCROLL. Often the same as:
Hans Boehm5e802f32015-06-22 17:18:52 -070090 private int mLsdOffset; // Position of least-significant digit in result
91 private int mLastDisplayedOffset; // Offset of last digit actually displayed after adding
Hans Boehmf6dae112015-06-18 17:57:50 -070092 // exponent.
Hans Boehm24c91ed2016-06-30 18:53:44 -070093 private boolean mWholePartFits; // Scientific notation not needed for initial display.
94 private float mNoExponentCredit;
95 // Fraction of digit width saved by avoiding scientific notation.
96 // Only accessed from UI thread.
97 private boolean mAppendExponent;
98 // The result fits entirely in the display, even with an exponent,
99 // but not with grouping separators. Since the result is not
100 // scrollable, and we do not add the exponent to max. scroll position,
101 // append an exponent insteadd of replacing trailing digits.
Justin Klaassen44595162015-05-28 17:55:20 -0700102 private final Object mWidthLock = new Object();
Hans Boehm24c91ed2016-06-30 18:53:44 -0700103 // Protects the next five fields. These fields are only
Hans Boehm3465c782016-12-12 17:28:10 -0800104 // updated by the UI thread, and read accesses by the UI thread
Hans Boehm24c91ed2016-06-30 18:53:44 -0700105 // sometimes do not acquire the lock.
Hans Boehmd4959e82016-11-15 18:01:28 -0800106 private int mWidthConstraint = 0;
Hans Boehma0e45f32015-05-30 13:20:35 -0700107 // Our total width in pixels minus space for ellipsis.
Hans Boehmd4959e82016-11-15 18:01:28 -0800108 // 0 ==> uninitialized.
Justin Klaassen44595162015-05-28 17:55:20 -0700109 private float mCharWidth = 1;
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700110 // Maximum character width. For now we pretend that all characters
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700111 // have this width.
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700112 // TODO: We're not really using a fixed width font. But it appears
113 // to be close enough for the characters we use that the difference
114 // is not noticeable.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700115 private float mGroupingSeparatorWidthRatio;
116 // Fraction of digit width occupied by a digit separator.
117 private float mDecimalCredit;
118 // Fraction of digit width saved by replacing digit with decimal point.
119 private float mNoEllipsisCredit;
120 // Fraction of digit width saved by both replacing ellipsis with digit
121 // and avoiding scientific notation.
Hans Boehmbd01e4b2016-11-23 10:12:58 -0800122 @Retention(RetentionPolicy.SOURCE)
123 @IntDef({SHOULD_REQUIRE, SHOULD_EVALUATE, SHOULD_NOT_EVALUATE})
124 public @interface EvaluationRequest {}
125 public static final int SHOULD_REQUIRE = 2;
126 public static final int SHOULD_EVALUATE = 1;
127 public static final int SHOULD_NOT_EVALUATE = 0;
128 @EvaluationRequest private int mEvaluationRequest = SHOULD_REQUIRE;
129 // Should we evaluate when layout completes, and how?
Hans Boehmd4959e82016-11-15 18:01:28 -0800130 private Evaluator.EvaluationListener mEvaluationListener = this;
131 // Listener to use if/when evaluation is requested.
Hans Boehm50ed3202015-06-09 14:35:49 -0700132 public static final int MAX_LEADING_ZEROES = 6;
Hans Boehma0e45f32015-05-30 13:20:35 -0700133 // Maximum number of leading zeroes after decimal point before we
134 // switch to scientific notation with negative exponent.
Hans Boehm50ed3202015-06-09 14:35:49 -0700135 public static final int MAX_TRAILING_ZEROES = 6;
Hans Boehma0e45f32015-05-30 13:20:35 -0700136 // Maximum number of trailing zeroes before the decimal point before
137 // we switch to scientific notation with positive exponent.
138 private static final int SCI_NOTATION_EXTRA = 1;
139 // Extra digits for standard scientific notation. In this case we
Hans Boehm80018c82015-08-02 16:59:07 -0700140 // have a decimal point and no ellipsis.
141 // We assume that we do not drop digits to make room for the decimal
142 // point in ordinary scientific notation. Thus >= 1.
Hans Boehm65a99a42016-02-03 18:16:07 -0800143 private static final int MAX_COPY_EXTRA = 100;
144 // The number of extra digits we are willing to compute to copy
145 // a result as an exact number.
146 private static final int MAX_RECOMPUTE_DIGITS = 2000;
147 // The maximum number of digits we're willing to recompute in the UI
148 // thread. We only do this for known rational results, where we
149 // can bound the computation cost.
Chenjie Yu3937b652016-06-01 23:14:26 -0700150 private final ForegroundColorSpan mExponentColorSpan;
151 private final BackgroundColorSpan mHighlightSpan;
Hans Boehm65a99a42016-02-03 18:16:07 -0800152
Hans Boehm1176f232015-05-11 16:26:03 -0700153 private ActionMode mActionMode;
Chenjie Yu3937b652016-06-01 23:14:26 -0700154 private ActionMode.Callback mCopyActionModeCallback;
155 private ContextMenu mContextMenu;
Hans Boehm84614952014-11-25 18:46:17 -0800156
Annie Chin37c33b62016-11-22 14:46:28 -0800157 // The user requested that the result currently being evaluated should be stored to "memory".
158 private boolean mStoreToMemoryRequested = false;
159
Hans Boehm84614952014-11-25 18:46:17 -0800160 public CalculatorResult(Context context, AttributeSet attrs) {
161 super(context, attrs);
162 mScroller = new OverScroller(context);
Chenjie Yu3937b652016-06-01 23:14:26 -0700163 mHighlightSpan = new BackgroundColorSpan(getHighlightColor());
164 mExponentColorSpan = new ForegroundColorSpan(
165 ContextCompat.getColor(context, R.color.display_result_exponent_text_color));
Hans Boehm84614952014-11-25 18:46:17 -0800166 mGestureDetector = new GestureDetector(context,
167 new GestureDetector.SimpleOnGestureListener() {
168 @Override
Justin Klaassend48b7562015-04-16 16:51:38 -0700169 public boolean onDown(MotionEvent e) {
170 return true;
171 }
172 @Override
Annie Chinc5b6e4f2016-12-05 13:34:14 -0800173 public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
174 float velocityY) {
Hans Boehm84614952014-11-25 18:46:17 -0800175 if (!mScroller.isFinished()) {
176 mCurrentPos = mScroller.getFinalX();
177 }
178 mScroller.forceFinished(true);
Chenjie Yu3937b652016-06-01 23:14:26 -0700179 stopActionModeOrContextMenu();
Hans Boehmfbcef702015-04-27 18:07:47 -0700180 CalculatorResult.this.cancelLongPress();
181 // Ignore scrolls of error string, etc.
182 if (!mScrollable) return true;
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700183 mScroller.fling(mCurrentPos, 0, - (int) velocityX, 0 /* horizontal only */,
Hans Boehm61568a12015-05-18 18:25:41 -0700184 mMinPos, mMaxPos, 0, 0);
Justin Klaassen44595162015-05-28 17:55:20 -0700185 postInvalidateOnAnimation();
Hans Boehm84614952014-11-25 18:46:17 -0800186 return true;
187 }
188 @Override
Annie Chinc5b6e4f2016-12-05 13:34:14 -0800189 public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
190 float distanceY) {
Hans Boehm61568a12015-05-18 18:25:41 -0700191 int distance = (int)distanceX;
Hans Boehm84614952014-11-25 18:46:17 -0800192 if (!mScroller.isFinished()) {
193 mCurrentPos = mScroller.getFinalX();
194 }
195 mScroller.forceFinished(true);
Chenjie Yu3937b652016-06-01 23:14:26 -0700196 stopActionModeOrContextMenu();
Hans Boehm84614952014-11-25 18:46:17 -0800197 CalculatorResult.this.cancelLongPress();
198 if (!mScrollable) return true;
Hans Boehm61568a12015-05-18 18:25:41 -0700199 if (mCurrentPos + distance < mMinPos) {
200 distance = mMinPos - mCurrentPos;
201 } else if (mCurrentPos + distance > mMaxPos) {
202 distance = mMaxPos - mCurrentPos;
203 }
Hans Boehm84614952014-11-25 18:46:17 -0800204 int duration = (int)(e2.getEventTime() - e1.getEventTime());
205 if (duration < 1 || duration > 100) duration = 10;
Hans Boehm61568a12015-05-18 18:25:41 -0700206 mScroller.startScroll(mCurrentPos, 0, distance, 0, (int)duration);
Justin Klaassen44595162015-05-28 17:55:20 -0700207 postInvalidateOnAnimation();
Hans Boehm84614952014-11-25 18:46:17 -0800208 return true;
209 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700210 @Override
211 public void onLongPress(MotionEvent e) {
Hans Boehm1176f232015-05-11 16:26:03 -0700212 if (mValid) {
Justin Klaassen3a05c7e2016-03-04 12:40:02 -0800213 performLongClick();
Hans Boehm1176f232015-05-11 16:26:03 -0700214 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700215 }
Hans Boehm84614952014-11-25 18:46:17 -0800216 });
Annie Chin5e737532016-12-05 17:15:48 -0800217
218 final int slop = ViewConfiguration.get(context).getScaledTouchSlop();
Justin Klaassen3a05c7e2016-03-04 12:40:02 -0800219 setOnTouchListener(new View.OnTouchListener() {
Annie Chin5e737532016-12-05 17:15:48 -0800220
221 // Used to determine whether a touch event should be intercepted.
222 private float mInitialDownX;
223 private float mInitialDownY;
224
Justin Klaassen3a05c7e2016-03-04 12:40:02 -0800225 @Override
226 public boolean onTouch(View v, MotionEvent event) {
Annie Chinc5b6e4f2016-12-05 13:34:14 -0800227 final int action = event.getActionMasked();
228
229 final float x = event.getX();
230 final float y = event.getY();
231 switch (action) {
232 case MotionEvent.ACTION_DOWN:
233 mInitialDownX = x;
234 mInitialDownY = y;
235 break;
236 case MotionEvent.ACTION_MOVE:
237 final float deltaX = Math.abs(x - mInitialDownX);
238 final float deltaY = Math.abs(y - mInitialDownY);
Annie Chinc5b6e4f2016-12-05 13:34:14 -0800239 if (deltaX > slop && deltaX > deltaY) {
240 // Prevent the DragLayout from intercepting horizontal scrolls.
241 getParent().requestDisallowInterceptTouchEvent(true);
242 }
243 }
Justin Klaassen3a05c7e2016-03-04 12:40:02 -0800244 return mGestureDetector.onTouchEvent(event);
245 }
246 });
Hans Boehm14344ff2016-06-08 13:01:51 -0700247
Chenjie Yu3937b652016-06-01 23:14:26 -0700248 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
249 setupActionMode();
250 } else {
251 setupContextMenu();
252 }
Hans Boehm14344ff2016-06-08 13:01:51 -0700253
Hans Boehm84614952014-11-25 18:46:17 -0800254 setCursorVisible(false);
Christine Franksafe28bb2016-07-29 17:24:52 -0700255 setLongClickable(false);
Christine Franks6f6c24a2016-09-08 18:21:47 -0700256 setContentDescription(context.getString(R.string.desc_result));
Hans Boehm84614952014-11-25 18:46:17 -0800257 }
258
Hans Boehm8f051c32016-10-03 16:53:58 -0700259 void setEvaluator(Evaluator evaluator, long index) {
Hans Boehm84614952014-11-25 18:46:17 -0800260 mEvaluator = evaluator;
Hans Boehm8f051c32016-10-03 16:53:58 -0700261 mIndex = index;
Annie Chin7c586042016-11-18 15:57:37 -0800262 requestLayout();
Hans Boehm84614952014-11-25 18:46:17 -0800263 }
264
Hans Boehmcd72f7e2016-06-01 16:21:25 -0700265 // Compute maximum digit width the hard way.
266 private static float getMaxDigitWidth(TextPaint paint) {
267 // Compute the maximum advance width for each digit, thus accounting for between-character
268 // spaces. If we ever support other kinds of digits, we may have to avoid kerning effects
269 // that could reduce the advance width within this particular string.
270 final String allDigits = "0123456789";
271 final float[] widths = new float[allDigits.length()];
272 paint.getTextWidths(allDigits, widths);
273 float maxWidth = 0;
274 for (float x : widths) {
275 maxWidth = Math.max(x, maxWidth);
276 }
277 return maxWidth;
278 }
279
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700280 @Override
281 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Justin Klaassend06f51d2016-08-03 00:41:31 -0700282 if (!isLaidOut()) {
Annie Chin45e59972016-12-02 14:46:09 -0800283 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Justin Klaassend06f51d2016-08-03 00:41:31 -0700284 // Set a minimum height so scaled error messages won't affect our layout.
285 setMinimumHeight(getLineHeight() + getCompoundPaddingBottom()
286 + getCompoundPaddingTop());
287 }
288
Justin Klaassen44595162015-05-28 17:55:20 -0700289 final TextPaint paint = getPaint();
Hans Boehm80018c82015-08-02 16:59:07 -0700290 final Context context = getContext();
Hans Boehmcd72f7e2016-06-01 16:21:25 -0700291 final float newCharWidth = getMaxDigitWidth(paint);
Hans Boehm80018c82015-08-02 16:59:07 -0700292 // Digits are presumed to have no more than newCharWidth.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700293 // There are two instances when we know that the result is otherwise narrower than
294 // expected:
295 // 1. For standard scientific notation (our type 1), we know that we have a norrow decimal
296 // point and no (usually wide) ellipsis symbol. We allow one extra digit
297 // (SCI_NOTATION_EXTRA) to compensate, and consider that in determining available width.
298 // 2. If we are using digit grouping separators and a decimal point, we give ourselves
299 // a fractional extra space for those separators, the value of which depends on whether
300 // there is also an ellipsis.
301 //
302 // Maximum extra space we need in various cases:
303 // Type 1 scientific notation, assuming ellipsis, minus sign and E are wider than a digit:
304 // Two minus signs + "E" + "." - 3 digits.
305 // Type 2 scientific notation:
306 // Ellipsis + "E" + "-" - 3 digits.
307 // In the absence of scientific notation, we may need a little less space.
308 // We give ourselves a bit of extra credit towards comma insertion and give
309 // ourselves more if we have either
310 // No ellipsis, or
311 // A decimal separator.
312
313 // Calculate extra space we need to reserve, in addition to character count.
Hans Boehm80018c82015-08-02 16:59:07 -0700314 final float decimalSeparatorWidth = Layout.getDesiredWidth(
315 context.getString(R.string.dec_point), paint);
Hans Boehm24c91ed2016-06-30 18:53:44 -0700316 final float minusWidth = Layout.getDesiredWidth(context.getString(R.string.op_sub), paint);
317 final float minusExtraWidth = Math.max(minusWidth - newCharWidth, 0.0f);
318 final float ellipsisWidth = Layout.getDesiredWidth(KeyMaps.ELLIPSIS, paint);
319 final float ellipsisExtraWidth = Math.max(ellipsisWidth - newCharWidth, 0.0f);
320 final float expWidth = Layout.getDesiredWidth(KeyMaps.translateResult("e"), paint);
321 final float expExtraWidth = Math.max(expWidth - newCharWidth, 0.0f);
322 final float type1Extra = 2 * minusExtraWidth + expExtraWidth + decimalSeparatorWidth;
323 final float type2Extra = ellipsisExtraWidth + expExtraWidth + minusExtraWidth;
324 final float extraWidth = Math.max(type1Extra, type2Extra);
325 final int intExtraWidth = (int) Math.ceil(extraWidth) + 1 /* to cover rounding sins */;
Hans Boehm80018c82015-08-02 16:59:07 -0700326 final int newWidthConstraint = MeasureSpec.getSize(widthMeasureSpec)
Hans Boehm24c91ed2016-06-30 18:53:44 -0700327 - (getPaddingLeft() + getPaddingRight()) - intExtraWidth;
328
329 // Calculate other width constants we need to handle grouping separators.
330 final float groupingSeparatorW =
331 Layout.getDesiredWidth(KeyMaps.translateResult(","), paint);
332 // Credits in the absence of any scientific notation:
333 float noExponentCredit = extraWidth - Math.max(ellipsisExtraWidth, minusExtraWidth);
334 final float noEllipsisCredit = extraWidth - minusExtraWidth; // includes noExponentCredit.
335 final float decimalCredit = Math.max(newCharWidth - decimalSeparatorWidth, 0.0f);
336
337 mNoExponentCredit = noExponentCredit / newCharWidth;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700338 synchronized(mWidthLock) {
Hans Boehm013969e2015-04-13 20:29:47 -0700339 mWidthConstraint = newWidthConstraint;
340 mCharWidth = newCharWidth;
Hans Boehm24c91ed2016-06-30 18:53:44 -0700341 mNoEllipsisCredit = noEllipsisCredit / newCharWidth;
342 mDecimalCredit = decimalCredit / newCharWidth;
343 mGroupingSeparatorWidthRatio = groupingSeparatorW / newCharWidth;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700344 }
Hans Boehm14344ff2016-06-08 13:01:51 -0700345
346 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700347 }
348
Annie Chin06fd3cf2016-11-07 16:04:33 -0800349 @Override
350 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
351 super.onLayout(changed, left, top, right, bottom);
352
Hans Boehmbd01e4b2016-11-23 10:12:58 -0800353 if (mEvaluator != null && mEvaluationRequest != SHOULD_NOT_EVALUATE) {
Annie Chin06fd3cf2016-11-07 16:04:33 -0800354 final CalculatorExpr expr = mEvaluator.getExpr(mIndex);
355 if (expr != null && expr.hasInterestingOps()) {
Hans Boehmbd01e4b2016-11-23 10:12:58 -0800356 if (mEvaluationRequest == SHOULD_REQUIRE) {
357 mEvaluator.requireResult(mIndex, mEvaluationListener, this);
358 } else {
359 mEvaluator.evaluateAndNotify(mIndex, mEvaluationListener, this);
360 }
Annie Chin06fd3cf2016-11-07 16:04:33 -0800361 }
362 }
363 }
364
Hans Boehmbd01e4b2016-11-23 10:12:58 -0800365 /**
366 * Specify whether we should evaluate result on layout.
367 * @param should one of SHOULD_REQUIRE, SHOULD_EVALUATE, SHOULD_NOT_EVALUATE
368 */
369 public void setShouldEvaluateResult(@EvaluationRequest int request,
370 Evaluator.EvaluationListener listener) {
Hans Boehmd4959e82016-11-15 18:01:28 -0800371 mEvaluationListener = listener;
Hans Boehmbd01e4b2016-11-23 10:12:58 -0800372 mEvaluationRequest = request;
Annie Chinbc001882016-11-09 19:41:21 -0800373 }
374
Hans Boehm8f051c32016-10-03 16:53:58 -0700375 // From Evaluator.CharMetricsInfo.
376 @Override
Hans Boehm24c91ed2016-06-30 18:53:44 -0700377 public float separatorChars(String s, int len) {
378 int start = 0;
379 while (start < len && !Character.isDigit(s.charAt(start))) {
380 ++start;
381 }
382 // We assume the rest consists of digits, and for consistency with the rest
383 // of the code, we assume all digits have width mCharWidth.
384 final int nDigits = len - start;
385 // We currently insert a digit separator every three digits.
386 final int nSeparators = (nDigits - 1) / 3;
387 synchronized(mWidthLock) {
388 // Always return an upper bound, even in the presence of rounding errors.
389 return nSeparators * mGroupingSeparatorWidthRatio;
390 }
391 }
392
Hans Boehm8f051c32016-10-03 16:53:58 -0700393 // From Evaluator.CharMetricsInfo.
394 @Override
Hans Boehm24c91ed2016-06-30 18:53:44 -0700395 public float getNoEllipsisCredit() {
396 synchronized(mWidthLock) {
397 return mNoEllipsisCredit;
398 }
399 }
400
Hans Boehm8f051c32016-10-03 16:53:58 -0700401 // From Evaluator.CharMetricsInfo.
402 @Override
Hans Boehm24c91ed2016-06-30 18:53:44 -0700403 public float getDecimalCredit() {
404 synchronized(mWidthLock) {
405 return mDecimalCredit;
406 }
407 }
408
Hans Boehma0e45f32015-05-30 13:20:35 -0700409 // Return the length of the exponent representation for the given exponent, in
410 // characters.
411 private final int expLen(int exp) {
412 if (exp == 0) return 0;
Hans Boehm5e802f32015-06-22 17:18:52 -0700413 final int abs_exp_digits = (int) Math.ceil(Math.log10(Math.abs((double)exp))
414 + 0.0000000001d /* Round whole numbers to next integer */);
415 return abs_exp_digits + (exp >= 0 ? 1 : 2);
Hans Boehm61568a12015-05-18 18:25:41 -0700416 }
417
Hans Boehma0e45f32015-05-30 13:20:35 -0700418 /**
419 * Initiate display of a new result.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700420 * Only called from UI thread.
Hans Boehma0e45f32015-05-30 13:20:35 -0700421 * The parameters specify various properties of the result.
Hans Boehm8f051c32016-10-03 16:53:58 -0700422 * @param index Index of expression that was just evaluated. Currently ignored, since we only
423 * expect notification for the expression result being displayed.
Hans Boehma0e45f32015-05-30 13:20:35 -0700424 * @param initPrec Initial display precision computed by evaluator. (1 = tenths digit)
425 * @param msd Position of most significant digit. Offset from left of string.
426 Evaluator.INVALID_MSD if unknown.
427 * @param leastDigPos Position of least significant digit (1 = tenths digit)
428 * or Integer.MAX_VALUE.
429 * @param truncatedWholePart Result up to but not including decimal point.
430 Currently we only use the length.
431 */
Hans Boehm8f051c32016-10-03 16:53:58 -0700432 @Override
433 public void onEvaluate(long index, int initPrec, int msd, int leastDigPos,
434 String truncatedWholePart) {
Hans Boehma0e45f32015-05-30 13:20:35 -0700435 initPositions(initPrec, msd, leastDigPos, truncatedWholePart);
Annie Chin37c33b62016-11-22 14:46:28 -0800436
437 if (mStoreToMemoryRequested) {
438 mEvaluator.copyToMemory(index);
439 mStoreToMemoryRequested = false;
440 }
Hans Boehm84614952014-11-25 18:46:17 -0800441 redisplay();
442 }
443
Hans Boehma0e45f32015-05-30 13:20:35 -0700444 /**
Annie Chin37c33b62016-11-22 14:46:28 -0800445 * Store the result for this index if it is available.
446 * If it is unavailable, set mStoreToMemoryRequested to indicate that we should store
447 * when evaluation is complete.
448 */
449 public void onMemoryStore() {
450 if (mEvaluator.hasResult(mIndex)) {
451 mEvaluator.copyToMemory(mIndex);
452 } else {
453 mStoreToMemoryRequested = true;
454 mEvaluator.requireResult(mIndex, this /* listener */, this /* CharMetricsInfo */);
455 }
456 }
457
458 /**
Christine Franks1d99be12016-11-14 14:00:36 -0800459 * Add the result to the value currently in memory.
460 */
461 public void onMemoryAdd() {
Christine Franksff8e0d82016-11-23 12:28:26 -0800462 mEvaluator.addToMemory(mIndex);
Christine Franks1d99be12016-11-14 14:00:36 -0800463 }
464
465 /**
466 * Subtract the result from the value currently in memory.
467 */
468 public void onMemorySubtract() {
Christine Franksff8e0d82016-11-23 12:28:26 -0800469 mEvaluator.subtractFromMemory(mIndex);
Christine Franks1d99be12016-11-14 14:00:36 -0800470 }
471
472 /**
Hans Boehm5e802f32015-06-22 17:18:52 -0700473 * Set up scroll bounds (mMinPos, mMaxPos, etc.) and determine whether the result is
474 * scrollable, based on the supplied information about the result.
Hans Boehma0e45f32015-05-30 13:20:35 -0700475 * This is unfortunately complicated because we need to predict whether trailing digits
476 * will eventually be replaced by an exponent.
477 * Just appending the exponent during formatting would be simpler, but would produce
478 * jumpier results during transitions.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700479 * Only called from UI thread.
Hans Boehma0e45f32015-05-30 13:20:35 -0700480 */
Hans Boehm5e802f32015-06-22 17:18:52 -0700481 private void initPositions(int initPrecOffset, int msdIndex, int lsdOffset,
482 String truncatedWholePart) {
Hans Boehma0e45f32015-05-30 13:20:35 -0700483 int maxChars = getMaxChars();
Hans Boehm24c91ed2016-06-30 18:53:44 -0700484 mWholeLen = truncatedWholePart.length();
485 // Allow a tiny amount of slop for associativity/rounding differences in length
486 // calculation. If getPreferredPrec() decided it should fit, we want to make it fit, too.
487 // We reserved one extra pixel, so the extra length is OK.
488 final int nSeparatorChars = (int) Math.ceil(
489 separatorChars(truncatedWholePart, truncatedWholePart.length())
490 - getNoEllipsisCredit() - 0.0001f);
491 mWholePartFits = mWholeLen + nSeparatorChars <= maxChars;
Hans Boehma0e45f32015-05-30 13:20:35 -0700492 mLastPos = INVALID;
Hans Boehm5e802f32015-06-22 17:18:52 -0700493 mLsdOffset = lsdOffset;
Hans Boehm24c91ed2016-06-30 18:53:44 -0700494 mAppendExponent = false;
Hans Boehma0e45f32015-05-30 13:20:35 -0700495 // Prevent scrolling past initial position, which is calculated to show leading digits.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700496 mCurrentPos = mMinPos = (int) Math.round(initPrecOffset * mCharWidth);
Hans Boehm5e802f32015-06-22 17:18:52 -0700497 if (msdIndex == Evaluator.INVALID_MSD) {
Hans Boehma0e45f32015-05-30 13:20:35 -0700498 // Possible zero value
Hans Boehm5e802f32015-06-22 17:18:52 -0700499 if (lsdOffset == Integer.MIN_VALUE) {
Hans Boehma0e45f32015-05-30 13:20:35 -0700500 // Definite zero value.
501 mMaxPos = mMinPos;
Hans Boehm24c91ed2016-06-30 18:53:44 -0700502 mMaxCharOffset = (int) Math.round(mMaxPos/mCharWidth);
Hans Boehma0e45f32015-05-30 13:20:35 -0700503 mScrollable = false;
504 } else {
505 // May be very small nonzero value. Allow user to find out.
Hans Boehm5e802f32015-06-22 17:18:52 -0700506 mMaxPos = mMaxCharOffset = MAX_RIGHT_SCROLL;
Hans Boehm24c91ed2016-06-30 18:53:44 -0700507 mMinPos -= mCharWidth; // Allow for future minus sign.
Hans Boehma0e45f32015-05-30 13:20:35 -0700508 mScrollable = true;
509 }
510 return;
511 }
Hans Boehma0e45f32015-05-30 13:20:35 -0700512 int negative = truncatedWholePart.charAt(0) == '-' ? 1 : 0;
Hans Boehm65a99a42016-02-03 18:16:07 -0800513 if (msdIndex > mWholeLen && msdIndex <= mWholeLen + 3) {
Hans Boehm5e802f32015-06-22 17:18:52 -0700514 // Avoid tiny negative exponent; pretend msdIndex is just to the right of decimal point.
Hans Boehm65a99a42016-02-03 18:16:07 -0800515 msdIndex = mWholeLen - 1;
Hans Boehma0e45f32015-05-30 13:20:35 -0700516 }
Hans Boehm24c91ed2016-06-30 18:53:44 -0700517 // Set to position of leftmost significant digit relative to dec. point. Usually negative.
Hans Boehm65a99a42016-02-03 18:16:07 -0800518 int minCharOffset = msdIndex - mWholeLen;
Hans Boehm5e802f32015-06-22 17:18:52 -0700519 if (minCharOffset > -1 && minCharOffset < MAX_LEADING_ZEROES + 2) {
Hans Boehma0e45f32015-05-30 13:20:35 -0700520 // Small number of leading zeroes, avoid scientific notation.
Hans Boehm5e802f32015-06-22 17:18:52 -0700521 minCharOffset = -1;
Hans Boehma0e45f32015-05-30 13:20:35 -0700522 }
Hans Boehm5e802f32015-06-22 17:18:52 -0700523 if (lsdOffset < MAX_RIGHT_SCROLL) {
524 mMaxCharOffset = lsdOffset;
525 if (mMaxCharOffset < -1 && mMaxCharOffset > -(MAX_TRAILING_ZEROES + 2)) {
526 mMaxCharOffset = -1;
Hans Boehma0e45f32015-05-30 13:20:35 -0700527 }
Hans Boehm5e802f32015-06-22 17:18:52 -0700528 // lsdOffset is positive or negative, never 0.
529 int currentExpLen = 0; // Length of required standard scientific notation exponent.
530 if (mMaxCharOffset < -1) {
531 currentExpLen = expLen(-minCharOffset - 1);
532 } else if (minCharOffset > -1 || mMaxCharOffset >= maxChars) {
Hans Boehm24c91ed2016-06-30 18:53:44 -0700533 // Number is either entirely to the right of decimal point, or decimal point is
534 // not visible when scrolled to the right.
Hans Boehm5e802f32015-06-22 17:18:52 -0700535 currentExpLen = expLen(-minCharOffset);
Hans Boehma0e45f32015-05-30 13:20:35 -0700536 }
Hans Boehm24c91ed2016-06-30 18:53:44 -0700537 // Exponent length does not included added decimal point. But whenever we add a
538 // decimal point, we allow an extra character (SCI_NOTATION_EXTRA).
539 final int separatorLength = mWholePartFits && minCharOffset < -3 ? nSeparatorChars : 0;
540 mScrollable = (mMaxCharOffset + currentExpLen + separatorLength - minCharOffset
541 + negative >= maxChars);
542 // Now adjust mMaxCharOffset for any required exponent.
Hans Boehm5e802f32015-06-22 17:18:52 -0700543 int newMaxCharOffset;
544 if (currentExpLen > 0) {
545 if (mScrollable) {
546 // We'll use exponent corresponding to leastDigPos when scrolled to right.
547 newMaxCharOffset = mMaxCharOffset + expLen(-lsdOffset);
548 } else {
549 newMaxCharOffset = mMaxCharOffset + currentExpLen;
550 }
551 if (mMaxCharOffset <= -1 && newMaxCharOffset > -1) {
552 // Very unlikely; just drop exponent.
553 mMaxCharOffset = -1;
554 } else {
Hans Boehm24c91ed2016-06-30 18:53:44 -0700555 mMaxCharOffset = Math.min(newMaxCharOffset, MAX_RIGHT_SCROLL);
Hans Boehma0e45f32015-05-30 13:20:35 -0700556 }
Hans Boehm24c91ed2016-06-30 18:53:44 -0700557 mMaxPos = Math.min((int) Math.round(mMaxCharOffset * mCharWidth),
558 MAX_RIGHT_SCROLL);
559 } else if (!mWholePartFits && !mScrollable) {
560 // Corner case in which entire number fits, but not with grouping separators. We
561 // will use an exponent in un-scrolled position, which may hide digits. Scrolling
562 // by one character will remove the exponent and reveal the last digits. Note
563 // that in the forced scientific notation case, the exponent length is not
564 // factored into mMaxCharOffset, since we do not want such an increase to impact
565 // scrolling behavior. In the unscrollable case, we thus have to append the
566 // exponent at the end using the forcePrecision argument to formatResult, in order
567 // to ensure that we get the entire result.
568 mScrollable = (mMaxCharOffset + expLen(-minCharOffset - 1) - minCharOffset
569 + negative >= maxChars);
570 if (mScrollable) {
571 mMaxPos = (int) Math.ceil(mMinPos + mCharWidth);
572 // Single character scroll will remove exponent and show remaining piece.
573 } else {
574 mMaxPos = mMinPos;
575 mAppendExponent = true;
576 }
577 } else {
578 mMaxPos = Math.min((int) Math.round(mMaxCharOffset * mCharWidth),
579 MAX_RIGHT_SCROLL);
Hans Boehma0e45f32015-05-30 13:20:35 -0700580 }
Hans Boehma0e45f32015-05-30 13:20:35 -0700581 if (!mScrollable) {
582 // Position the number consistently with our assumptions to make sure it
583 // actually fits.
584 mCurrentPos = mMaxPos;
585 }
586 } else {
Hans Boehm5e802f32015-06-22 17:18:52 -0700587 mMaxPos = mMaxCharOffset = MAX_RIGHT_SCROLL;
Hans Boehma0e45f32015-05-30 13:20:35 -0700588 mScrollable = true;
589 }
590 }
591
Hans Boehm24c91ed2016-06-30 18:53:44 -0700592 /**
593 * Display error message indicated by resourceId.
594 * UI thread only.
595 */
Hans Boehm8f051c32016-10-03 16:53:58 -0700596 @Override
597 public void onError(long index, int resourceId) {
Annie Chin37c33b62016-11-22 14:46:28 -0800598 mStoreToMemoryRequested = false;
Hans Boehm760a9dc2015-04-20 10:27:12 -0700599 mValid = true;
Christine Franksafe28bb2016-07-29 17:24:52 -0700600 setLongClickable(false);
Hans Boehm84614952014-11-25 18:46:17 -0800601 mScrollable = false;
Hans Boehm14344ff2016-06-08 13:01:51 -0700602 final String msg = getContext().getString(resourceId);
Hans Boehm14344ff2016-06-08 13:01:51 -0700603 final float measuredWidth = Layout.getDesiredWidth(msg, getPaint());
Hans Boehm24c91ed2016-06-30 18:53:44 -0700604 if (measuredWidth > mWidthConstraint) {
Hans Boehm14344ff2016-06-08 13:01:51 -0700605 // Multiply by .99 to avoid rounding effects.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700606 final float scaleFactor = 0.99f * mWidthConstraint / measuredWidth;
Hans Boehm14344ff2016-06-08 13:01:51 -0700607 final RelativeSizeSpan smallTextSpan = new RelativeSizeSpan(scaleFactor);
608 final SpannableString scaledMsg = new SpannableString(msg);
609 scaledMsg.setSpan(smallTextSpan, 0, msg.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
610 setText(scaledMsg);
611 } else {
612 setText(msg);
613 }
Hans Boehm84614952014-11-25 18:46:17 -0800614 }
615
Hans Boehm013969e2015-04-13 20:29:47 -0700616 private final int MAX_COPY_SIZE = 1000000;
617
Hans Boehma0e45f32015-05-30 13:20:35 -0700618 /*
619 * Return the most significant digit position in the given string or Evaluator.INVALID_MSD.
Hans Boehm3666e632015-07-27 18:33:12 -0700620 * Unlike Evaluator.getMsdIndexOf, we treat a final 1 as significant.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700621 * Pure function; callable from anywhere.
Hans Boehma0e45f32015-05-30 13:20:35 -0700622 */
Hans Boehm3666e632015-07-27 18:33:12 -0700623 public static int getNaiveMsdIndexOf(String s) {
Hans Boehm65a99a42016-02-03 18:16:07 -0800624 final int len = s.length();
Hans Boehma0e45f32015-05-30 13:20:35 -0700625 for (int i = 0; i < len; ++i) {
626 char c = s.charAt(i);
627 if (c != '-' && c != '.' && c != '0') {
628 return i;
629 }
630 }
631 return Evaluator.INVALID_MSD;
632 }
633
Hans Boehm24c91ed2016-06-30 18:53:44 -0700634 /**
635 * Format a result returned by Evaluator.getString() into a single line containing ellipses
636 * (if appropriate) and an exponent (if appropriate).
637 * We add two distinct kinds of exponents:
638 * (1) If the final result contains the leading digit we use standard scientific notation.
639 * (2) If not, we add an exponent corresponding to an interpretation of the final result as
640 * an integer.
641 * We add an ellipsis on the left if the result was truncated.
642 * We add ellipses and exponents in a way that leaves most digits in the position they
643 * would have been in had we not done so. This minimizes jumps as a result of scrolling.
644 * Result is NOT internationalized, uses "E" for exponent.
645 * Called only from UI thread; We sometimes omit locking for fields.
646 * @param precOffset The value that was passed to getString. Identifies the significance of
647 the rightmost digit. A value of 1 means the rightmost digits corresponds to tenths.
648 * @param maxDigs The maximum number of characters in the result
649 * @param truncated The in parameter was already truncated, beyond possibly removing the
650 minus sign.
651 * @param negative The in parameter represents a negative result. (Minus sign may be removed
652 without setting truncated.)
653 * @param lastDisplayedOffset If not null, we set lastDisplayedOffset[0] to the offset of
654 the last digit actually appearing in the display.
655 * @param forcePrecision If true, we make sure that the last displayed digit corresponds to
656 precOffset, and allow maxDigs to be exceeded in adding the exponent and commas.
657 * @param forceSciNotation Force scientific notation. May be set because we don't have
658 space for grouping separators, but whole number otherwise fits.
659 * @param insertCommas Insert commas (literally, not internationalized) as digit separators.
660 We only ever do this for the integral part of a number, and only when no
661 exponent is displayed in the initial position. The combination of which means
662 that we only do it when no exponent is displayed.
663 We insert commas in a way that does consider the width of the actual localized digit
664 separator. Commas count towards maxDigs as the appropriate fraction of a digit.
665 */
666 private String formatResult(String in, int precOffset, int maxDigs, boolean truncated,
667 boolean negative, int lastDisplayedOffset[], boolean forcePrecision,
668 boolean forceSciNotation, boolean insertCommas) {
Hans Boehm5e802f32015-06-22 17:18:52 -0700669 final int minusSpace = negative ? 1 : 0;
Hans Boehm3666e632015-07-27 18:33:12 -0700670 final int msdIndex = truncated ? -1 : getNaiveMsdIndexOf(in); // INVALID_MSD is OK.
Hans Boehm5e802f32015-06-22 17:18:52 -0700671 String result = in;
Hans Boehm24c91ed2016-06-30 18:53:44 -0700672 boolean needEllipsis = false;
Hans Boehm73ecff22015-09-03 16:04:50 -0700673 if (truncated || (negative && result.charAt(0) != '-')) {
Hans Boehm24c91ed2016-06-30 18:53:44 -0700674 needEllipsis = true;
Hans Boehm73ecff22015-09-03 16:04:50 -0700675 result = KeyMaps.ELLIPSIS + result.substring(1, result.length());
676 // Ellipsis may be removed again in the type(1) scientific notation case.
677 }
678 final int decIndex = result.indexOf('.');
Hans Boehm65a99a42016-02-03 18:16:07 -0800679 if (lastDisplayedOffset != null) {
680 lastDisplayedOffset[0] = precOffset;
681 }
Hans Boehm24c91ed2016-06-30 18:53:44 -0700682 if (forceSciNotation || (decIndex == -1 || msdIndex != Evaluator.INVALID_MSD
Hans Boehm5e802f32015-06-22 17:18:52 -0700683 && msdIndex - decIndex > MAX_LEADING_ZEROES + 1) && precOffset != -1) {
Hans Boehm24c91ed2016-06-30 18:53:44 -0700684 // Either:
685 // 1) No decimal point displayed, and it's not just to the right of the last digit, or
686 // 2) we are at the front of a number whos integral part is too large to allow
687 // comma insertion, or
688 // 3) we should suppress leading zeroes.
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700689 // Add an exponent to let the user track which digits are currently displayed.
Hans Boehm5e802f32015-06-22 17:18:52 -0700690 // Start with type (2) exponent if we dropped no digits. -1 accounts for decimal point.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700691 // We currently never show digit separators together with an exponent.
Hans Boehm5e802f32015-06-22 17:18:52 -0700692 final int initExponent = precOffset > 0 ? -precOffset : -precOffset - 1;
693 int exponent = initExponent;
Hans Boehm08e8f322015-04-21 13:18:38 -0700694 boolean hasPoint = false;
Hans Boehm5e802f32015-06-22 17:18:52 -0700695 if (!truncated && msdIndex < maxDigs - 1
696 && result.length() - msdIndex + 1 + minusSpace
697 <= maxDigs + SCI_NOTATION_EXTRA) {
698 // Type (1) exponent computation and transformation:
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700699 // Leading digit is in display window. Use standard calculator scientific notation
700 // with one digit to the left of the decimal point. Insert decimal point and
701 // delete leading zeroes.
Hans Boehma0e45f32015-05-30 13:20:35 -0700702 // We try to keep leading digits roughly in position, and never
Hans Boehmf6dae112015-06-18 17:57:50 -0700703 // lengthen the result by more than SCI_NOTATION_EXTRA.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700704 if (decIndex > msdIndex) {
705 // In the forceSciNotation, we can have a decimal point in the relevant digit
706 // range. Remove it.
707 result = result.substring(0, decIndex)
708 + result.substring(decIndex + 1, result.length());
709 // msdIndex and precOffset unaffected.
710 }
Hans Boehm5e802f32015-06-22 17:18:52 -0700711 final int resLen = result.length();
712 String fraction = result.substring(msdIndex + 1, resLen);
713 result = (negative ? "-" : "") + result.substring(msdIndex, msdIndex + 1)
714 + "." + fraction;
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700715 // Original exp was correct for decimal point at right of fraction.
716 // Adjust by length of fraction.
Hans Boehm5e802f32015-06-22 17:18:52 -0700717 exponent = initExponent + resLen - msdIndex - 1;
Hans Boehm08e8f322015-04-21 13:18:38 -0700718 hasPoint = true;
719 }
Hans Boehm73ecff22015-09-03 16:04:50 -0700720 // Exponent can't be zero.
721 // Actually add the exponent of either type:
722 if (!forcePrecision) {
723 int dropDigits; // Digits to drop to make room for exponent.
724 if (hasPoint) {
725 // Type (1) exponent.
726 // Drop digits even if there is room. Otherwise the scrolling gets jumpy.
727 dropDigits = expLen(exponent);
728 if (dropDigits >= result.length() - 1) {
729 // Jumpy is better than no mantissa. Probably impossible anyway.
730 dropDigits = Math.max(result.length() - 2, 0);
Hans Boehma0e45f32015-05-30 13:20:35 -0700731 }
Hans Boehm73ecff22015-09-03 16:04:50 -0700732 } else {
733 // Type (2) exponent.
734 // Exponent depends on the number of digits we drop, which depends on
735 // exponent ...
736 for (dropDigits = 2; expLen(initExponent + dropDigits) > dropDigits;
737 ++dropDigits) {}
738 exponent = initExponent + dropDigits;
739 if (precOffset - dropDigits > mLsdOffset) {
740 // This can happen if e.g. result = 10^40 + 10^10
741 // It turns out we would otherwise display ...10e9 because it takes
742 // the same amount of space as ...1e10 but shows one more digit.
743 // But we don't want to display a trailing zero, even if it's free.
744 ++dropDigits;
745 ++exponent;
746 }
Hans Boehm08e8f322015-04-21 13:18:38 -0700747 }
Hans Boehm64751002017-02-02 17:09:18 -0800748 if (dropDigits >= result.length() - 1) {
749 // Display too small to show meaningful result.
750 return KeyMaps.ELLIPSIS + "E" + KeyMaps.ELLIPSIS;
751 }
Hans Boehm73ecff22015-09-03 16:04:50 -0700752 result = result.substring(0, result.length() - dropDigits);
Hans Boehm65a99a42016-02-03 18:16:07 -0800753 if (lastDisplayedOffset != null) {
754 lastDisplayedOffset[0] -= dropDigits;
755 }
Hans Boehm73ecff22015-09-03 16:04:50 -0700756 }
757 result = result + "E" + Integer.toString(exponent);
Hans Boehm24c91ed2016-06-30 18:53:44 -0700758 } else if (insertCommas) {
759 // Add commas to the whole number section, and then truncate on left to fit,
760 // counting commas as a fractional digit.
761 final int wholeStart = needEllipsis ? 1 : 0;
762 int orig_length = result.length();
763 final float nCommaChars;
764 if (decIndex != -1) {
765 nCommaChars = separatorChars(result, decIndex);
766 result = StringUtils.addCommas(result, wholeStart, decIndex)
767 + result.substring(decIndex, orig_length);
768 } else {
769 nCommaChars = separatorChars(result, orig_length);
770 result = StringUtils.addCommas(result, wholeStart, orig_length);
771 }
772 if (needEllipsis) {
773 orig_length -= 1; // Exclude ellipsis.
774 }
775 final float len = orig_length + nCommaChars;
776 int deletedChars = 0;
777 final float ellipsisCredit = getNoEllipsisCredit();
778 final float decimalCredit = getDecimalCredit();
779 final float effectiveLen = len - (decIndex == -1 ? 0 : getDecimalCredit());
780 final float ellipsisAdjustment =
781 needEllipsis ? mNoExponentCredit : getNoEllipsisCredit();
782 // As above, we allow for a tiny amount of extra length here, for consistency with
783 // getPreferredPrec().
784 if (effectiveLen - ellipsisAdjustment > (float) (maxDigs - wholeStart) + 0.0001f
785 && !forcePrecision) {
786 float deletedWidth = 0.0f;
787 while (effectiveLen - mNoExponentCredit - deletedWidth
788 > (float) (maxDigs - 1 /* for ellipsis */)) {
789 if (result.charAt(deletedChars) == ',') {
790 deletedWidth += mGroupingSeparatorWidthRatio;
791 } else {
792 deletedWidth += 1.0f;
793 }
794 deletedChars++;
795 }
796 }
797 if (deletedChars > 0) {
798 result = KeyMaps.ELLIPSIS + result.substring(deletedChars, result.length());
799 } else if (needEllipsis) {
800 result = KeyMaps.ELLIPSIS + result;
801 }
Hans Boehm5e802f32015-06-22 17:18:52 -0700802 }
803 return result;
Hans Boehm08e8f322015-04-21 13:18:38 -0700804 }
805
Hans Boehmf6dae112015-06-18 17:57:50 -0700806 /**
807 * Get formatted, but not internationalized, result from mEvaluator.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700808 * @param precOffset requested position (1 = tenths) of last included digit
809 * @param maxSize maximum number of characters (more or less) in result
810 * @param lastDisplayedOffset zeroth entry is set to actual offset of last included digit,
Hans Boehm65a99a42016-02-03 18:16:07 -0800811 * after adjusting for exponent, etc. May be null.
Hans Boehmf6dae112015-06-18 17:57:50 -0700812 * @param forcePrecision Ensure that last included digit is at pos, at the expense
813 * of treating maxSize as a soft limit.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700814 * @param forceSciNotation Force scientific notation, even if not required by maxSize.
815 * @param insertCommas Insert commas as digit separators.
Hans Boehmf6dae112015-06-18 17:57:50 -0700816 */
Hans Boehm5e802f32015-06-22 17:18:52 -0700817 private String getFormattedResult(int precOffset, int maxSize, int lastDisplayedOffset[],
Hans Boehm24c91ed2016-06-30 18:53:44 -0700818 boolean forcePrecision, boolean forceSciNotation, boolean insertCommas) {
Hans Boehm08e8f322015-04-21 13:18:38 -0700819 final boolean truncated[] = new boolean[1];
820 final boolean negative[] = new boolean[1];
Hans Boehm5e802f32015-06-22 17:18:52 -0700821 final int requestedPrecOffset[] = {precOffset};
Hans Boehm8f051c32016-10-03 16:53:58 -0700822 final String rawResult = mEvaluator.getString(mIndex, requestedPrecOffset, mMaxCharOffset,
823 maxSize, truncated, negative, this);
Hans Boehm5e802f32015-06-22 17:18:52 -0700824 return formatResult(rawResult, requestedPrecOffset[0], maxSize, truncated[0], negative[0],
Hans Boehm24c91ed2016-06-30 18:53:44 -0700825 lastDisplayedOffset, forcePrecision, forceSciNotation, insertCommas);
Hans Boehm08e8f322015-04-21 13:18:38 -0700826 }
827
Hans Boehm65a99a42016-02-03 18:16:07 -0800828 /**
829 * Return entire result (within reason) up to current displayed precision.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700830 * @param withSeparators Add digit separators
Hans Boehm65a99a42016-02-03 18:16:07 -0800831 */
Hans Boehm24c91ed2016-06-30 18:53:44 -0700832 public String getFullText(boolean withSeparators) {
Hans Boehm760a9dc2015-04-20 10:27:12 -0700833 if (!mValid) return "";
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700834 if (!mScrollable) return getText().toString();
Hans Boehm5e802f32015-06-22 17:18:52 -0700835 return KeyMaps.translateResult(getFormattedResult(mLastDisplayedOffset, MAX_COPY_SIZE,
Hans Boehm24c91ed2016-06-30 18:53:44 -0700836 null, true /* forcePrecision */, false /* forceSciNotation */, withSeparators));
Hans Boehm84614952014-11-25 18:46:17 -0800837 }
838
Hans Boehm24c91ed2016-06-30 18:53:44 -0700839 /**
840 * Did the above produce a correct result?
841 * UI thread only.
842 */
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700843 public boolean fullTextIsExact() {
Hans Boehm3465c782016-12-12 17:28:10 -0800844 return !mScrollable || (getCharOffset(mMaxPos) == getCharOffset(mCurrentPos)
Hans Boehm24c91ed2016-06-30 18:53:44 -0700845 && mMaxCharOffset != MAX_RIGHT_SCROLL);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700846 }
847
Hans Boehm61568a12015-05-18 18:25:41 -0700848 /**
Hans Boehm65a99a42016-02-03 18:16:07 -0800849 * Get entire result up to current displayed precision, or up to MAX_COPY_EXTRA additional
850 * digits, if it will lead to an exact result.
851 */
852 public String getFullCopyText() {
853 if (!mValid
854 || mLsdOffset == Integer.MAX_VALUE
855 || fullTextIsExact()
856 || mWholeLen > MAX_RECOMPUTE_DIGITS
857 || mWholeLen + mLsdOffset > MAX_RECOMPUTE_DIGITS
858 || mLsdOffset - mLastDisplayedOffset > MAX_COPY_EXTRA) {
Hans Boehm24c91ed2016-06-30 18:53:44 -0700859 return getFullText(false /* withSeparators */);
Hans Boehm65a99a42016-02-03 18:16:07 -0800860 }
861 // It's reasonable to compute and copy the exact result instead.
Hans Boehm3465c782016-12-12 17:28:10 -0800862 int fractionLsdOffset = Math.max(0, mLsdOffset);
863 String rawResult = mEvaluator.getResult(mIndex).toStringTruncated(fractionLsdOffset);
864 if (mLsdOffset <= -1) {
865 // Result has trailing decimal point. Remove it.
866 rawResult = rawResult.substring(0, rawResult.length() - 1);
867 fractionLsdOffset = -1;
868 }
869 final String formattedResult = formatResult(rawResult, fractionLsdOffset, MAX_COPY_SIZE,
Hans Boehm24c91ed2016-06-30 18:53:44 -0700870 false, rawResult.charAt(0) == '-', null, true /* forcePrecision */,
871 false /* forceSciNotation */, false /* insertCommas */);
Hans Boehm65a99a42016-02-03 18:16:07 -0800872 return KeyMaps.translateResult(formattedResult);
873 }
874
875 /**
Hans Boehm61568a12015-05-18 18:25:41 -0700876 * Return the maximum number of characters that will fit in the result display.
Hans Boehm8f051c32016-10-03 16:53:58 -0700877 * May be called asynchronously from non-UI thread. From Evaluator.CharMetricsInfo.
Hans Boehmd4959e82016-11-15 18:01:28 -0800878 * Returns zero if measurement hasn't completed.
Hans Boehm61568a12015-05-18 18:25:41 -0700879 */
Hans Boehm8f051c32016-10-03 16:53:58 -0700880 @Override
881 public int getMaxChars() {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700882 int result;
883 synchronized(mWidthLock) {
Hans Boehmd4959e82016-11-15 18:01:28 -0800884 return (int) Math.floor(mWidthConstraint / mCharWidth);
Hans Boehm84614952014-11-25 18:46:17 -0800885 }
886 }
887
Hans Boehm61568a12015-05-18 18:25:41 -0700888 /**
Justin Klaassen44595162015-05-28 17:55:20 -0700889 * @return {@code true} if the currently displayed result is scrollable
Hans Boehm61568a12015-05-18 18:25:41 -0700890 */
Justin Klaassen44595162015-05-28 17:55:20 -0700891 public boolean isScrollable() {
892 return mScrollable;
Hans Boehm61568a12015-05-18 18:25:41 -0700893 }
894
Hans Boehm24c91ed2016-06-30 18:53:44 -0700895 /**
896 * Map pixel position to digit offset.
897 * UI thread only.
898 */
899 int getCharOffset(int pos) {
900 return (int) Math.round(pos / mCharWidth); // Lock not needed.
Hans Boehm013969e2015-04-13 20:29:47 -0700901 }
902
Hans Boehm84614952014-11-25 18:46:17 -0800903 void clear() {
Hans Boehm760a9dc2015-04-20 10:27:12 -0700904 mValid = false;
Hans Boehm1176f232015-05-11 16:26:03 -0700905 mScrollable = false;
Hans Boehm84614952014-11-25 18:46:17 -0800906 setText("");
Christine Franksafe28bb2016-07-29 17:24:52 -0700907 setLongClickable(false);
Hans Boehm84614952014-11-25 18:46:17 -0800908 }
909
Hans Boehm8f051c32016-10-03 16:53:58 -0700910 @Override
911 public void onCancelled(long index) {
912 clear();
Annie Chin37c33b62016-11-22 14:46:28 -0800913 mStoreToMemoryRequested = false;
Hans Boehm8f051c32016-10-03 16:53:58 -0700914 }
915
Hans Boehm24c91ed2016-06-30 18:53:44 -0700916 /**
917 * Refresh display.
Hans Boehm8f051c32016-10-03 16:53:58 -0700918 * Only called in UI thread. Index argument is currently ignored.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700919 */
Hans Boehm8f051c32016-10-03 16:53:58 -0700920 @Override
921 public void onReevaluate(long index) {
922 redisplay();
923 }
924
925 public void redisplay() {
Hans Boehm64751002017-02-02 17:09:18 -0800926 int maxChars = getMaxChars();
927 if (maxChars < 4) {
928 // Display currently too small to display a reasonable result. Punt to avoid crash.
929 return;
930 }
Christine Franks6f6c24a2016-09-08 18:21:47 -0700931 if (mScroller.isFinished() && length() > 0) {
Christine Franksd21205c2016-08-04 10:06:15 -0700932 setAccessibilityLiveRegion(ACCESSIBILITY_LIVE_REGION_POLITE);
933 }
Hans Boehm24c91ed2016-06-30 18:53:44 -0700934 int currentCharOffset = getCharOffset(mCurrentPos);
Hans Boehm5e802f32015-06-22 17:18:52 -0700935 int lastDisplayedOffset[] = new int[1];
Hans Boehm24c91ed2016-06-30 18:53:44 -0700936 String result = getFormattedResult(currentCharOffset, maxChars, lastDisplayedOffset,
937 mAppendExponent /* forcePrecision; preserve entire result */,
938 !mWholePartFits
939 && currentCharOffset == getCharOffset(mMinPos) /* forceSciNotation */,
940 mWholePartFits /* insertCommas */ );
Hans Boehm0b9806f2015-06-29 16:07:15 -0700941 int expIndex = result.indexOf('E');
Hans Boehm013969e2015-04-13 20:29:47 -0700942 result = KeyMaps.translateResult(result);
Hans Boehm5e802f32015-06-22 17:18:52 -0700943 if (expIndex > 0 && result.indexOf('.') == -1) {
Hans Boehm84614952014-11-25 18:46:17 -0800944 // Gray out exponent if used as position indicator
945 SpannableString formattedResult = new SpannableString(result);
Hans Boehm5e802f32015-06-22 17:18:52 -0700946 formattedResult.setSpan(mExponentColorSpan, expIndex, result.length(),
Hans Boehm84614952014-11-25 18:46:17 -0800947 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
948 setText(formattedResult);
949 } else {
950 setText(result);
951 }
Hans Boehm5e802f32015-06-22 17:18:52 -0700952 mLastDisplayedOffset = lastDisplayedOffset[0];
Hans Boehm760a9dc2015-04-20 10:27:12 -0700953 mValid = true;
Christine Franksafe28bb2016-07-29 17:24:52 -0700954 setLongClickable(true);
Hans Boehm84614952014-11-25 18:46:17 -0800955 }
956
957 @Override
Christine Franks6f6c24a2016-09-08 18:21:47 -0700958 protected void onTextChanged(java.lang.CharSequence text, int start, int lengthBefore,
959 int lengthAfter) {
960 super.onTextChanged(text, start, lengthBefore, lengthAfter);
961
962 if (!mScrollable || mScroller.isFinished()) {
963 if (lengthBefore == 0 && lengthAfter > 0) {
964 setAccessibilityLiveRegion(ACCESSIBILITY_LIVE_REGION_POLITE);
965 setContentDescription(null);
966 } else if (lengthBefore > 0 && lengthAfter == 0) {
967 setAccessibilityLiveRegion(ACCESSIBILITY_LIVE_REGION_NONE);
968 setContentDescription(getContext().getString(R.string.desc_result));
969 }
970 }
971 }
972
973 @Override
Hans Boehm84614952014-11-25 18:46:17 -0800974 public void computeScroll() {
Christine Franks6f6c24a2016-09-08 18:21:47 -0700975 if (!mScrollable) {
976 return;
977 }
978
Hans Boehm84614952014-11-25 18:46:17 -0800979 if (mScroller.computeScrollOffset()) {
980 mCurrentPos = mScroller.getCurrX();
Hans Boehm24c91ed2016-06-30 18:53:44 -0700981 if (getCharOffset(mCurrentPos) != getCharOffset(mLastPos)) {
Hans Boehm84614952014-11-25 18:46:17 -0800982 mLastPos = mCurrentPos;
983 redisplay();
984 }
Christine Franks6f6c24a2016-09-08 18:21:47 -0700985 }
986
987 if (!mScroller.isFinished()) {
Justin Klaassen44595162015-05-28 17:55:20 -0700988 postInvalidateOnAnimation();
Christine Franksd21205c2016-08-04 10:06:15 -0700989 setAccessibilityLiveRegion(ACCESSIBILITY_LIVE_REGION_NONE);
Christine Franks6f6c24a2016-09-08 18:21:47 -0700990 } else if (length() > 0){
Christine Franksd21205c2016-08-04 10:06:15 -0700991 setAccessibilityLiveRegion(ACCESSIBILITY_LIVE_REGION_POLITE);
Hans Boehm84614952014-11-25 18:46:17 -0800992 }
993 }
994
Chenjie Yu3937b652016-06-01 23:14:26 -0700995 /**
Christine Franks1d99be12016-11-14 14:00:36 -0800996 * Use ActionMode for copy/memory support on M and higher.
Chenjie Yu3937b652016-06-01 23:14:26 -0700997 */
998 @TargetApi(Build.VERSION_CODES.M)
999 private void setupActionMode() {
1000 mCopyActionModeCallback = new ActionMode.Callback2() {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001001
Chenjie Yu3937b652016-06-01 23:14:26 -07001002 @Override
1003 public boolean onCreateActionMode(ActionMode mode, Menu menu) {
1004 final MenuInflater inflater = mode.getMenuInflater();
Christine Franks7485df52016-12-01 13:18:45 -08001005 return createContextMenu(inflater, menu);
Chenjie Yu3937b652016-06-01 23:14:26 -07001006 }
Hans Boehm7f83e362015-06-10 15:41:04 -07001007
Chenjie Yu3937b652016-06-01 23:14:26 -07001008 @Override
1009 public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
1010 return false; // Return false if nothing is done
1011 }
Hans Boehm7f83e362015-06-10 15:41:04 -07001012
Chenjie Yu3937b652016-06-01 23:14:26 -07001013 @Override
1014 public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
1015 if (onMenuItemClick(item)) {
Hans Boehm65a99a42016-02-03 18:16:07 -08001016 mode.finish();
1017 return true;
Chenjie Yu3937b652016-06-01 23:14:26 -07001018 } else {
1019 return false;
Hans Boehm65a99a42016-02-03 18:16:07 -08001020 }
Chenjie Yu3937b652016-06-01 23:14:26 -07001021 }
1022
1023 @Override
1024 public void onDestroyActionMode(ActionMode mode) {
1025 unhighlightResult();
1026 mActionMode = null;
1027 }
1028
1029 @Override
1030 public void onGetContentRect(ActionMode mode, View view, Rect outRect) {
1031 super.onGetContentRect(mode, view, outRect);
1032
1033 outRect.left += view.getPaddingLeft();
1034 outRect.top += view.getPaddingTop();
1035 outRect.right -= view.getPaddingRight();
1036 outRect.bottom -= view.getPaddingBottom();
1037 final int width = (int) Layout.getDesiredWidth(getText(), getPaint());
1038 if (width < outRect.width()) {
1039 outRect.left = outRect.right - width;
1040 }
1041
1042 if (!BuildCompat.isAtLeastN()) {
1043 // The CAB (prior to N) only takes the translation of a view into account, so
1044 // if a scale is applied to the view then the offset outRect will end up being
1045 // positioned incorrectly. We workaround that limitation by manually applying
1046 // the scale to the outRect, which the CAB will then offset to the correct
1047 // position.
1048 final float scaleX = view.getScaleX();
1049 final float scaleY = view.getScaleY();
1050 outRect.left *= scaleX;
1051 outRect.right *= scaleX;
1052 outRect.top *= scaleY;
1053 outRect.bottom *= scaleY;
1054 }
1055 }
1056 };
1057 setOnLongClickListener(new View.OnLongClickListener() {
1058 @Override
1059 public boolean onLongClick(View v) {
1060 if (mValid) {
1061 mActionMode = startActionMode(mCopyActionModeCallback,
1062 ActionMode.TYPE_FLOATING);
1063 return true;
1064 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001065 return false;
1066 }
Chenjie Yu3937b652016-06-01 23:14:26 -07001067 });
1068 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001069
Chenjie Yu3937b652016-06-01 23:14:26 -07001070 /**
Christine Franks1d99be12016-11-14 14:00:36 -08001071 * Use ContextMenu for copy/memory support on L and lower.
Chenjie Yu3937b652016-06-01 23:14:26 -07001072 */
1073 private void setupContextMenu() {
1074 setOnCreateContextMenuListener(new OnCreateContextMenuListener() {
1075 @Override
1076 public void onCreateContextMenu(ContextMenu contextMenu, View view,
1077 ContextMenu.ContextMenuInfo contextMenuInfo) {
1078 final MenuInflater inflater = new MenuInflater(getContext());
Christine Franks7485df52016-12-01 13:18:45 -08001079 createContextMenu(inflater, contextMenu);
Chenjie Yu3937b652016-06-01 23:14:26 -07001080 mContextMenu = contextMenu;
Hans Boehm9c160b42016-12-02 11:55:12 -08001081 for (int i = 0; i < contextMenu.size(); i ++) {
Chenjie Yu3937b652016-06-01 23:14:26 -07001082 contextMenu.getItem(i).setOnMenuItemClickListener(CalculatorResult.this);
1083 }
Hans Boehm7f83e362015-06-10 15:41:04 -07001084 }
Chenjie Yu3937b652016-06-01 23:14:26 -07001085 });
1086 setOnLongClickListener(new View.OnLongClickListener() {
1087 @Override
1088 public boolean onLongClick(View v) {
1089 if (mValid) {
1090 return showContextMenu();
1091 }
1092 return false;
Justin Klaassenf1b61f42016-04-27 16:00:11 -07001093 }
Chenjie Yu3937b652016-06-01 23:14:26 -07001094 });
1095 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001096
Christine Franks7485df52016-12-01 13:18:45 -08001097 private boolean createContextMenu(MenuInflater inflater, Menu menu) {
Christine Franks1d99be12016-11-14 14:00:36 -08001098 inflater.inflate(R.menu.menu_result, menu);
1099 final boolean displayMemory = mEvaluator.getMemoryIndex() != 0;
1100 final MenuItem memoryAddItem = menu.findItem(R.id.memory_add);
1101 final MenuItem memorySubtractItem = menu.findItem(R.id.memory_subtract);
1102 memoryAddItem.setEnabled(displayMemory);
1103 memorySubtractItem.setEnabled(displayMemory);
Chenjie Yu3937b652016-06-01 23:14:26 -07001104 highlightResult();
1105 return true;
1106 }
1107
1108 public boolean stopActionModeOrContextMenu() {
Hans Boehm1176f232015-05-11 16:26:03 -07001109 if (mActionMode != null) {
1110 mActionMode.finish();
1111 return true;
1112 }
Chenjie Yu3937b652016-06-01 23:14:26 -07001113 if (mContextMenu != null) {
1114 unhighlightResult();
1115 mContextMenu.close();
1116 return true;
1117 }
Hans Boehm1176f232015-05-11 16:26:03 -07001118 return false;
1119 }
1120
Chenjie Yu3937b652016-06-01 23:14:26 -07001121 private void highlightResult() {
1122 final Spannable text = (Spannable) getText();
1123 text.setSpan(mHighlightSpan, 0, text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
1124 }
1125
1126 private void unhighlightResult() {
1127 final Spannable text = (Spannable) getText();
1128 text.removeSpan(mHighlightSpan);
1129 }
1130
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001131 private void setPrimaryClip(ClipData clip) {
1132 ClipboardManager clipboard = (ClipboardManager) getContext().
Hans Boehmc01cd7f2015-05-12 18:32:19 -07001133 getSystemService(Context.CLIPBOARD_SERVICE);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001134 clipboard.setPrimaryClip(clip);
1135 }
1136
1137 private void copyContent() {
Hans Boehm65a99a42016-02-03 18:16:07 -08001138 final CharSequence text = getFullCopyText();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001139 ClipboardManager clipboard =
Hans Boehmc01cd7f2015-05-12 18:32:19 -07001140 (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
1141 // We include a tag URI, to allow us to recognize our own results and handle them
1142 // specially.
Hans Boehm8f051c32016-10-03 16:53:58 -07001143 ClipData.Item newItem = new ClipData.Item(text, null, mEvaluator.capture(mIndex));
Hans Boehmc01cd7f2015-05-12 18:32:19 -07001144 String[] mimeTypes = new String[] {ClipDescription.MIMETYPE_TEXT_PLAIN};
1145 ClipData cd = new ClipData("calculator result", mimeTypes, newItem);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001146 clipboard.setPrimaryClip(cd);
Hans Boehmc01cd7f2015-05-12 18:32:19 -07001147 Toast.makeText(getContext(), R.string.text_copied_toast, Toast.LENGTH_SHORT).show();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001148 }
1149
Chenjie Yu3937b652016-06-01 23:14:26 -07001150 @Override
1151 public boolean onMenuItemClick(MenuItem item) {
1152 switch (item.getItemId()) {
Christine Franks1d99be12016-11-14 14:00:36 -08001153 case R.id.memory_add:
1154 onMemoryAdd();
1155 return true;
1156 case R.id.memory_subtract:
1157 onMemorySubtract();
1158 return true;
1159 case R.id.memory_store:
1160 onMemoryStore();
1161 return true;
Chenjie Yu3937b652016-06-01 23:14:26 -07001162 case R.id.menu_copy:
Hans Boehm8f051c32016-10-03 16:53:58 -07001163 if (mEvaluator.evaluationInProgress(mIndex)) {
Chenjie Yu3937b652016-06-01 23:14:26 -07001164 // Refuse to copy placeholder characters.
1165 return false;
1166 } else {
1167 copyContent();
1168 unhighlightResult();
1169 return true;
1170 }
1171 default:
1172 return false;
1173 }
1174 }
Christine Franks7485df52016-12-01 13:18:45 -08001175
1176 @Override
1177 protected void onDetachedFromWindow() {
1178 stopActionModeOrContextMenu();
1179 super.onDetachedFromWindow();
1180 }
Hans Boehmbd01e4b2016-11-23 10:12:58 -08001181}