blob: 49308b7cfc53f8f6a858a0fd8dcef0ddc4c63393 [file] [log] [blame]
Hans Boehm84614952014-11-25 18:46:17 -08001/*
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07002 * Copyright (C) 2015 The Android Open Source Project
Hans Boehm84614952014-11-25 18:46:17 -08003 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.calculator2;
18
Chenjie Yu3937b652016-06-01 23:14:26 -070019import android.annotation.TargetApi;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070020import android.content.ClipData;
21import android.content.ClipDescription;
Justin Klaassen44595162015-05-28 17:55:20 -070022import android.content.ClipboardManager;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070023import android.content.Context;
Hans Boehm7f83e362015-06-10 15:41:04 -070024import android.graphics.Rect;
Chenjie Yu3937b652016-06-01 23:14:26 -070025import android.os.Build;
26import android.support.v4.content.ContextCompat;
Justin Klaassenf1b61f42016-04-27 16:00:11 -070027import android.support.v4.os.BuildCompat;
Justin Klaassen44595162015-05-28 17:55:20 -070028import android.text.Layout;
Hans Boehm7f83e362015-06-10 15:41:04 -070029import android.text.Spannable;
Hans Boehm84614952014-11-25 18:46:17 -080030import android.text.SpannableString;
Hans Boehm1176f232015-05-11 16:26:03 -070031import android.text.Spanned;
Justin Klaassen44595162015-05-28 17:55:20 -070032import android.text.TextPaint;
Hans Boehm7f83e362015-06-10 15:41:04 -070033import android.text.style.BackgroundColorSpan;
Hans Boehm84614952014-11-25 18:46:17 -080034import android.text.style.ForegroundColorSpan;
Hans Boehm14344ff2016-06-08 13:01:51 -070035import android.text.style.RelativeSizeSpan;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070036import android.util.AttributeSet;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070037import android.view.ActionMode;
Chenjie Yu3937b652016-06-01 23:14:26 -070038import android.view.ContextMenu;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070039import android.view.GestureDetector;
40import android.view.Menu;
41import android.view.MenuInflater;
42import android.view.MenuItem;
43import android.view.MotionEvent;
44import android.view.View;
Justin Klaassen44595162015-05-28 17:55:20 -070045import android.widget.OverScroller;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070046import android.widget.Toast;
Hans Boehm84614952014-11-25 18:46:17 -080047
Hans Boehm84614952014-11-25 18:46:17 -080048// A text widget that is "infinitely" scrollable to the right,
49// and obtains the text to display via a callback to Logic.
Chenjie Yu3937b652016-06-01 23:14:26 -070050public class CalculatorResult extends AlignedTextView implements MenuItem.OnMenuItemClickListener {
Hans Boehm61568a12015-05-18 18:25:41 -070051 static final int MAX_RIGHT_SCROLL = 10000000;
Hans Boehm08e8f322015-04-21 13:18:38 -070052 static final int INVALID = MAX_RIGHT_SCROLL + 10000;
Hans Boehm84614952014-11-25 18:46:17 -080053 // A larger value is unlikely to avoid running out of space
54 final OverScroller mScroller;
55 final GestureDetector mGestureDetector;
Hans Boehm84614952014-11-25 18:46:17 -080056 private Evaluator mEvaluator;
57 private boolean mScrollable = false;
58 // A scrollable result is currently displayed.
Hans Boehm760a9dc2015-04-20 10:27:12 -070059 private boolean mValid = false;
Hans Boehmc01cd7f2015-05-12 18:32:19 -070060 // The result holds something valid; either a a number or an error
61 // message.
Hans Boehm5e802f32015-06-22 17:18:52 -070062 // A suffix of "Pos" denotes a pixel offset. Zero represents a scroll position
63 // in which the decimal point is just barely visible on the right of the display.
Hans Boehmc01cd7f2015-05-12 18:32:19 -070064 private int mCurrentPos;// Position of right of display relative to decimal point, in pixels.
65 // Large positive values mean the decimal point is scrolled off the
66 // left of the display. Zero means decimal point is barely displayed
67 // on the right.
Hans Boehm61568a12015-05-18 18:25:41 -070068 private int mLastPos; // Position already reflected in display. Pixels.
Hans Boehm65a99a42016-02-03 18:16:07 -080069 private int mMinPos; // Minimum position to avoid unnecessary blanks on the left. Pixels.
Hans Boehm61568a12015-05-18 18:25:41 -070070 private int mMaxPos; // Maximum position before we start displaying the infinite
71 // sequence of trailing zeroes on the right. Pixels.
Hans Boehm65a99a42016-02-03 18:16:07 -080072 private int mWholeLen; // Length of the whole part of current result.
Hans Boehm5e802f32015-06-22 17:18:52 -070073 // In the following, we use a suffix of Offset to denote a character position in a numeric
74 // string relative to the decimal point. Positive is to the right and negative is to
75 // the left. 1 = tenths position, -1 = units. Integer.MAX_VALUE is sometimes used
76 // for the offset of the last digit in an a nonterminating decimal expansion.
77 // We use the suffix "Index" to denote a zero-based index into a string representing a
78 // result.
Hans Boehm5e802f32015-06-22 17:18:52 -070079 private int mMaxCharOffset; // Character offset from decimal point of rightmost digit
80 // that should be displayed. Essentially the same as
81 private int mLsdOffset; // Position of least-significant digit in result
82 private int mLastDisplayedOffset; // Offset of last digit actually displayed after adding
Hans Boehmf6dae112015-06-18 17:57:50 -070083 // exponent.
Justin Klaassen44595162015-05-28 17:55:20 -070084 private final Object mWidthLock = new Object();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070085 // Protects the next two fields.
86 private int mWidthConstraint = -1;
Hans Boehma0e45f32015-05-30 13:20:35 -070087 // Our total width in pixels minus space for ellipsis.
Justin Klaassen44595162015-05-28 17:55:20 -070088 private float mCharWidth = 1;
Hans Boehmc01cd7f2015-05-12 18:32:19 -070089 // Maximum character width. For now we pretend that all characters
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070090 // have this width.
Hans Boehmc01cd7f2015-05-12 18:32:19 -070091 // TODO: We're not really using a fixed width font. But it appears
92 // to be close enough for the characters we use that the difference
93 // is not noticeable.
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070094 private static final int MAX_WIDTH = 100;
95 // Maximum number of digits displayed
Hans Boehm50ed3202015-06-09 14:35:49 -070096 public static final int MAX_LEADING_ZEROES = 6;
Hans Boehma0e45f32015-05-30 13:20:35 -070097 // Maximum number of leading zeroes after decimal point before we
98 // switch to scientific notation with negative exponent.
Hans Boehm50ed3202015-06-09 14:35:49 -070099 public static final int MAX_TRAILING_ZEROES = 6;
Hans Boehma0e45f32015-05-30 13:20:35 -0700100 // Maximum number of trailing zeroes before the decimal point before
101 // we switch to scientific notation with positive exponent.
102 private static final int SCI_NOTATION_EXTRA = 1;
103 // Extra digits for standard scientific notation. In this case we
Hans Boehm80018c82015-08-02 16:59:07 -0700104 // have a decimal point and no ellipsis.
105 // We assume that we do not drop digits to make room for the decimal
106 // point in ordinary scientific notation. Thus >= 1.
Hans Boehm65a99a42016-02-03 18:16:07 -0800107 private static final int MAX_COPY_EXTRA = 100;
108 // The number of extra digits we are willing to compute to copy
109 // a result as an exact number.
110 private static final int MAX_RECOMPUTE_DIGITS = 2000;
111 // The maximum number of digits we're willing to recompute in the UI
112 // thread. We only do this for known rational results, where we
113 // can bound the computation cost.
Chenjie Yu3937b652016-06-01 23:14:26 -0700114 private final ForegroundColorSpan mExponentColorSpan;
115 private final BackgroundColorSpan mHighlightSpan;
Hans Boehm65a99a42016-02-03 18:16:07 -0800116
Hans Boehm1176f232015-05-11 16:26:03 -0700117 private ActionMode mActionMode;
Chenjie Yu3937b652016-06-01 23:14:26 -0700118 private ActionMode.Callback mCopyActionModeCallback;
119 private ContextMenu mContextMenu;
Hans Boehm84614952014-11-25 18:46:17 -0800120
121 public CalculatorResult(Context context, AttributeSet attrs) {
122 super(context, attrs);
123 mScroller = new OverScroller(context);
Chenjie Yu3937b652016-06-01 23:14:26 -0700124 mHighlightSpan = new BackgroundColorSpan(getHighlightColor());
125 mExponentColorSpan = new ForegroundColorSpan(
126 ContextCompat.getColor(context, R.color.display_result_exponent_text_color));
Hans Boehm84614952014-11-25 18:46:17 -0800127 mGestureDetector = new GestureDetector(context,
128 new GestureDetector.SimpleOnGestureListener() {
129 @Override
Justin Klaassend48b7562015-04-16 16:51:38 -0700130 public boolean onDown(MotionEvent e) {
131 return true;
132 }
133 @Override
Hans Boehm84614952014-11-25 18:46:17 -0800134 public boolean onFling(MotionEvent e1, MotionEvent e2,
135 float velocityX, float velocityY) {
136 if (!mScroller.isFinished()) {
137 mCurrentPos = mScroller.getFinalX();
138 }
139 mScroller.forceFinished(true);
Chenjie Yu3937b652016-06-01 23:14:26 -0700140 stopActionModeOrContextMenu();
Hans Boehmfbcef702015-04-27 18:07:47 -0700141 CalculatorResult.this.cancelLongPress();
142 // Ignore scrolls of error string, etc.
143 if (!mScrollable) return true;
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700144 mScroller.fling(mCurrentPos, 0, - (int) velocityX, 0 /* horizontal only */,
Hans Boehm61568a12015-05-18 18:25:41 -0700145 mMinPos, mMaxPos, 0, 0);
Justin Klaassen44595162015-05-28 17:55:20 -0700146 postInvalidateOnAnimation();
Hans Boehm84614952014-11-25 18:46:17 -0800147 return true;
148 }
149 @Override
150 public boolean onScroll(MotionEvent e1, MotionEvent e2,
151 float distanceX, float distanceY) {
Hans Boehm61568a12015-05-18 18:25:41 -0700152 int distance = (int)distanceX;
Hans Boehm84614952014-11-25 18:46:17 -0800153 if (!mScroller.isFinished()) {
154 mCurrentPos = mScroller.getFinalX();
155 }
156 mScroller.forceFinished(true);
Chenjie Yu3937b652016-06-01 23:14:26 -0700157 stopActionModeOrContextMenu();
Hans Boehm84614952014-11-25 18:46:17 -0800158 CalculatorResult.this.cancelLongPress();
159 if (!mScrollable) return true;
Hans Boehm61568a12015-05-18 18:25:41 -0700160 if (mCurrentPos + distance < mMinPos) {
161 distance = mMinPos - mCurrentPos;
162 } else if (mCurrentPos + distance > mMaxPos) {
163 distance = mMaxPos - mCurrentPos;
164 }
Hans Boehm84614952014-11-25 18:46:17 -0800165 int duration = (int)(e2.getEventTime() - e1.getEventTime());
166 if (duration < 1 || duration > 100) duration = 10;
Hans Boehm61568a12015-05-18 18:25:41 -0700167 mScroller.startScroll(mCurrentPos, 0, distance, 0, (int)duration);
Justin Klaassen44595162015-05-28 17:55:20 -0700168 postInvalidateOnAnimation();
Hans Boehm84614952014-11-25 18:46:17 -0800169 return true;
170 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700171 @Override
172 public void onLongPress(MotionEvent e) {
Hans Boehm1176f232015-05-11 16:26:03 -0700173 if (mValid) {
Justin Klaassen3a05c7e2016-03-04 12:40:02 -0800174 performLongClick();
Hans Boehm1176f232015-05-11 16:26:03 -0700175 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700176 }
Hans Boehm84614952014-11-25 18:46:17 -0800177 });
Justin Klaassen3a05c7e2016-03-04 12:40:02 -0800178 setOnTouchListener(new View.OnTouchListener() {
179 @Override
180 public boolean onTouch(View v, MotionEvent event) {
181 return mGestureDetector.onTouchEvent(event);
182 }
183 });
Hans Boehm14344ff2016-06-08 13:01:51 -0700184
Chenjie Yu3937b652016-06-01 23:14:26 -0700185 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
186 setupActionMode();
187 } else {
188 setupContextMenu();
189 }
Hans Boehm14344ff2016-06-08 13:01:51 -0700190
Hans Boehm84614952014-11-25 18:46:17 -0800191 setCursorVisible(false);
Hans Boehm14344ff2016-06-08 13:01:51 -0700192
193 // Set a minimum height so scaled error messages won't affect our layout.
194 setMinimumHeight(getLineHeight() + getCompoundPaddingBottom() + getCompoundPaddingTop());
Hans Boehm84614952014-11-25 18:46:17 -0800195 }
196
197 void setEvaluator(Evaluator evaluator) {
198 mEvaluator = evaluator;
199 }
200
Hans Boehmcd72f7e2016-06-01 16:21:25 -0700201 // Compute maximum digit width the hard way.
202 private static float getMaxDigitWidth(TextPaint paint) {
203 // Compute the maximum advance width for each digit, thus accounting for between-character
204 // spaces. If we ever support other kinds of digits, we may have to avoid kerning effects
205 // that could reduce the advance width within this particular string.
206 final String allDigits = "0123456789";
207 final float[] widths = new float[allDigits.length()];
208 paint.getTextWidths(allDigits, widths);
209 float maxWidth = 0;
210 for (float x : widths) {
211 maxWidth = Math.max(x, maxWidth);
212 }
213 return maxWidth;
214 }
215
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700216 @Override
217 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Justin Klaassen44595162015-05-28 17:55:20 -0700218 final TextPaint paint = getPaint();
Hans Boehm80018c82015-08-02 16:59:07 -0700219 final Context context = getContext();
Hans Boehmcd72f7e2016-06-01 16:21:25 -0700220 final float newCharWidth = getMaxDigitWidth(paint);
Hans Boehm80018c82015-08-02 16:59:07 -0700221 // Digits are presumed to have no more than newCharWidth.
222 // We sometimes replace a character by an ellipsis or, due to SCI_NOTATION_EXTRA, add
223 // an extra decimal separator beyond the maximum number of characters we normally allow.
224 // Empirically, our minus sign is also slightly wider than a digit, so we have to
225 // account for that. We never have both an ellipsis and two minus signs, and
226 // we assume an ellipsis is no narrower than a minus sign.
227 final float decimalSeparatorWidth = Layout.getDesiredWidth(
228 context.getString(R.string.dec_point), paint);
229 final float minusExtraWidth = Layout.getDesiredWidth(
230 context.getString(R.string.op_sub), paint) - newCharWidth;
231 final float ellipsisExtraWidth = Layout.getDesiredWidth(KeyMaps.ELLIPSIS, paint)
232 - newCharWidth;
233 final int extraWidth = (int) (Math.ceil(Math.max(decimalSeparatorWidth + minusExtraWidth,
234 ellipsisExtraWidth)) + Math.max(minusExtraWidth, 0.0f));
235 final int newWidthConstraint = MeasureSpec.getSize(widthMeasureSpec)
236 - (getPaddingLeft() + getPaddingRight()) - extraWidth;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700237 synchronized(mWidthLock) {
Hans Boehm013969e2015-04-13 20:29:47 -0700238 mWidthConstraint = newWidthConstraint;
239 mCharWidth = newCharWidth;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700240 }
Hans Boehm14344ff2016-06-08 13:01:51 -0700241
242 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700243 }
244
Hans Boehma0e45f32015-05-30 13:20:35 -0700245 // Return the length of the exponent representation for the given exponent, in
246 // characters.
247 private final int expLen(int exp) {
248 if (exp == 0) return 0;
Hans Boehm5e802f32015-06-22 17:18:52 -0700249 final int abs_exp_digits = (int) Math.ceil(Math.log10(Math.abs((double)exp))
250 + 0.0000000001d /* Round whole numbers to next integer */);
251 return abs_exp_digits + (exp >= 0 ? 1 : 2);
Hans Boehm61568a12015-05-18 18:25:41 -0700252 }
253
Hans Boehma0e45f32015-05-30 13:20:35 -0700254 /**
255 * Initiate display of a new result.
256 * The parameters specify various properties of the result.
257 * @param initPrec Initial display precision computed by evaluator. (1 = tenths digit)
258 * @param msd Position of most significant digit. Offset from left of string.
259 Evaluator.INVALID_MSD if unknown.
260 * @param leastDigPos Position of least significant digit (1 = tenths digit)
261 * or Integer.MAX_VALUE.
262 * @param truncatedWholePart Result up to but not including decimal point.
263 Currently we only use the length.
264 */
265 void displayResult(int initPrec, int msd, int leastDigPos, String truncatedWholePart) {
266 initPositions(initPrec, msd, leastDigPos, truncatedWholePart);
Hans Boehm84614952014-11-25 18:46:17 -0800267 redisplay();
268 }
269
Hans Boehma0e45f32015-05-30 13:20:35 -0700270 /**
Hans Boehm5e802f32015-06-22 17:18:52 -0700271 * Set up scroll bounds (mMinPos, mMaxPos, etc.) and determine whether the result is
272 * scrollable, based on the supplied information about the result.
Hans Boehma0e45f32015-05-30 13:20:35 -0700273 * This is unfortunately complicated because we need to predict whether trailing digits
274 * will eventually be replaced by an exponent.
275 * Just appending the exponent during formatting would be simpler, but would produce
276 * jumpier results during transitions.
277 */
Hans Boehm5e802f32015-06-22 17:18:52 -0700278 private void initPositions(int initPrecOffset, int msdIndex, int lsdOffset,
279 String truncatedWholePart) {
Hans Boehma0e45f32015-05-30 13:20:35 -0700280 float charWidth;
281 int maxChars = getMaxChars();
282 mLastPos = INVALID;
Hans Boehm5e802f32015-06-22 17:18:52 -0700283 mLsdOffset = lsdOffset;
Hans Boehma0e45f32015-05-30 13:20:35 -0700284 synchronized(mWidthLock) {
285 charWidth = mCharWidth;
286 }
Hans Boehm5e802f32015-06-22 17:18:52 -0700287 mCurrentPos = mMinPos = (int) Math.round(initPrecOffset * charWidth);
Hans Boehma0e45f32015-05-30 13:20:35 -0700288 // Prevent scrolling past initial position, which is calculated to show leading digits.
Hans Boehm5e802f32015-06-22 17:18:52 -0700289 if (msdIndex == Evaluator.INVALID_MSD) {
Hans Boehma0e45f32015-05-30 13:20:35 -0700290 // Possible zero value
Hans Boehm5e802f32015-06-22 17:18:52 -0700291 if (lsdOffset == Integer.MIN_VALUE) {
Hans Boehma0e45f32015-05-30 13:20:35 -0700292 // Definite zero value.
293 mMaxPos = mMinPos;
Hans Boehm5e802f32015-06-22 17:18:52 -0700294 mMaxCharOffset = (int) Math.round(mMaxPos/charWidth);
Hans Boehma0e45f32015-05-30 13:20:35 -0700295 mScrollable = false;
296 } else {
297 // May be very small nonzero value. Allow user to find out.
Hans Boehm5e802f32015-06-22 17:18:52 -0700298 mMaxPos = mMaxCharOffset = MAX_RIGHT_SCROLL;
299 mMinPos -= charWidth; // Allow for future minus sign.
Hans Boehma0e45f32015-05-30 13:20:35 -0700300 mScrollable = true;
301 }
302 return;
303 }
Hans Boehm65a99a42016-02-03 18:16:07 -0800304 mWholeLen = truncatedWholePart.length();
Hans Boehma0e45f32015-05-30 13:20:35 -0700305 int negative = truncatedWholePart.charAt(0) == '-' ? 1 : 0;
Hans Boehm65a99a42016-02-03 18:16:07 -0800306 if (msdIndex > mWholeLen && msdIndex <= mWholeLen + 3) {
Hans Boehm5e802f32015-06-22 17:18:52 -0700307 // Avoid tiny negative exponent; pretend msdIndex is just to the right of decimal point.
Hans Boehm65a99a42016-02-03 18:16:07 -0800308 msdIndex = mWholeLen - 1;
Hans Boehma0e45f32015-05-30 13:20:35 -0700309 }
Hans Boehm65a99a42016-02-03 18:16:07 -0800310 int minCharOffset = msdIndex - mWholeLen;
Hans Boehma0e45f32015-05-30 13:20:35 -0700311 // Position of leftmost significant digit relative to dec. point.
312 // Usually negative.
Hans Boehm5e802f32015-06-22 17:18:52 -0700313 mMaxCharOffset = MAX_RIGHT_SCROLL; // How far does it make sense to scroll right?
Hans Boehma0e45f32015-05-30 13:20:35 -0700314 // If msd is left of decimal point should logically be
315 // mMinPos = - (int) Math.ceil(getPaint().measureText(truncatedWholePart)), but
316 // we eventually translate to a character position by dividing by mCharWidth.
317 // To avoid rounding issues, we use the analogous computation here.
Hans Boehm5e802f32015-06-22 17:18:52 -0700318 if (minCharOffset > -1 && minCharOffset < MAX_LEADING_ZEROES + 2) {
Hans Boehma0e45f32015-05-30 13:20:35 -0700319 // Small number of leading zeroes, avoid scientific notation.
Hans Boehm5e802f32015-06-22 17:18:52 -0700320 minCharOffset = -1;
Hans Boehma0e45f32015-05-30 13:20:35 -0700321 }
Hans Boehm5e802f32015-06-22 17:18:52 -0700322 if (lsdOffset < MAX_RIGHT_SCROLL) {
323 mMaxCharOffset = lsdOffset;
324 if (mMaxCharOffset < -1 && mMaxCharOffset > -(MAX_TRAILING_ZEROES + 2)) {
325 mMaxCharOffset = -1;
Hans Boehma0e45f32015-05-30 13:20:35 -0700326 }
Hans Boehm5e802f32015-06-22 17:18:52 -0700327 // lsdOffset is positive or negative, never 0.
328 int currentExpLen = 0; // Length of required standard scientific notation exponent.
329 if (mMaxCharOffset < -1) {
330 currentExpLen = expLen(-minCharOffset - 1);
331 } else if (minCharOffset > -1 || mMaxCharOffset >= maxChars) {
Hans Boehma0e45f32015-05-30 13:20:35 -0700332 // Number either entirely to the right of decimal point, or decimal point not
333 // visible when scrolled to the right.
Hans Boehm5e802f32015-06-22 17:18:52 -0700334 currentExpLen = expLen(-minCharOffset);
Hans Boehma0e45f32015-05-30 13:20:35 -0700335 }
Hans Boehm5e802f32015-06-22 17:18:52 -0700336 mScrollable = (mMaxCharOffset + currentExpLen - minCharOffset + negative >= maxChars);
337 int newMaxCharOffset;
338 if (currentExpLen > 0) {
339 if (mScrollable) {
340 // We'll use exponent corresponding to leastDigPos when scrolled to right.
341 newMaxCharOffset = mMaxCharOffset + expLen(-lsdOffset);
342 } else {
343 newMaxCharOffset = mMaxCharOffset + currentExpLen;
344 }
345 if (mMaxCharOffset <= -1 && newMaxCharOffset > -1) {
346 // Very unlikely; just drop exponent.
347 mMaxCharOffset = -1;
348 } else {
349 mMaxCharOffset = newMaxCharOffset;
Hans Boehma0e45f32015-05-30 13:20:35 -0700350 }
351 }
Hans Boehm5e802f32015-06-22 17:18:52 -0700352 mMaxPos = Math.min((int) Math.round(mMaxCharOffset * charWidth), MAX_RIGHT_SCROLL);
Hans Boehma0e45f32015-05-30 13:20:35 -0700353 if (!mScrollable) {
354 // Position the number consistently with our assumptions to make sure it
355 // actually fits.
356 mCurrentPos = mMaxPos;
357 }
358 } else {
Hans Boehm5e802f32015-06-22 17:18:52 -0700359 mMaxPos = mMaxCharOffset = MAX_RIGHT_SCROLL;
Hans Boehma0e45f32015-05-30 13:20:35 -0700360 mScrollable = true;
361 }
362 }
363
Hans Boehm84614952014-11-25 18:46:17 -0800364 void displayError(int resourceId) {
Hans Boehm760a9dc2015-04-20 10:27:12 -0700365 mValid = true;
Hans Boehm84614952014-11-25 18:46:17 -0800366 mScrollable = false;
Hans Boehm14344ff2016-06-08 13:01:51 -0700367 final String msg = getContext().getString(resourceId);
368 final float widthConstraint;
369 synchronized(mWidthLock) {
370 widthConstraint = mWidthConstraint;
371 }
372 final float measuredWidth = Layout.getDesiredWidth(msg, getPaint());
373 if (measuredWidth > widthConstraint) {
374 // Multiply by .99 to avoid rounding effects.
375 final float scaleFactor = 0.99f * widthConstraint / measuredWidth;
376 final RelativeSizeSpan smallTextSpan = new RelativeSizeSpan(scaleFactor);
377 final SpannableString scaledMsg = new SpannableString(msg);
378 scaledMsg.setSpan(smallTextSpan, 0, msg.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
379 setText(scaledMsg);
380 } else {
381 setText(msg);
382 }
Hans Boehm84614952014-11-25 18:46:17 -0800383 }
384
Hans Boehm013969e2015-04-13 20:29:47 -0700385 private final int MAX_COPY_SIZE = 1000000;
386
Hans Boehma0e45f32015-05-30 13:20:35 -0700387 /*
388 * Return the most significant digit position in the given string or Evaluator.INVALID_MSD.
Hans Boehm3666e632015-07-27 18:33:12 -0700389 * Unlike Evaluator.getMsdIndexOf, we treat a final 1 as significant.
Hans Boehma0e45f32015-05-30 13:20:35 -0700390 */
Hans Boehm3666e632015-07-27 18:33:12 -0700391 public static int getNaiveMsdIndexOf(String s) {
Hans Boehm65a99a42016-02-03 18:16:07 -0800392 final int len = s.length();
Hans Boehma0e45f32015-05-30 13:20:35 -0700393 for (int i = 0; i < len; ++i) {
394 char c = s.charAt(i);
395 if (c != '-' && c != '.' && c != '0') {
396 return i;
397 }
398 }
399 return Evaluator.INVALID_MSD;
400 }
401
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700402 // Format a result returned by Evaluator.getString() into a single line containing ellipses
Hans Boehm3666e632015-07-27 18:33:12 -0700403 // (if appropriate) and an exponent (if appropriate). precOffset is the value that was passed
404 // to getString and thus identifies the significance of the rightmost digit.
Hans Boehma0e45f32015-05-30 13:20:35 -0700405 // A value of 1 means the rightmost digits corresponds to tenths.
406 // maxDigs is the maximum number of characters in the result.
Hans Boehm65a99a42016-02-03 18:16:07 -0800407 // If lastDisplayedOffset is not null, we set lastDisplayedOffset[0] to the offset of
408 // the last digit actually appearing in the display.
Hans Boehmf6dae112015-06-18 17:57:50 -0700409 // If forcePrecision is true, we make sure that the last displayed digit corresponds to
Hans Boehm65a99a42016-02-03 18:16:07 -0800410 // precOffset, and allow maxDigs to be exceeded in adding the exponent.
Hans Boehm08e8f322015-04-21 13:18:38 -0700411 // We add two distinct kinds of exponents:
Hans Boehm5e802f32015-06-22 17:18:52 -0700412 // (1) If the final result contains the leading digit we use standard scientific notation.
413 // (2) If not, we add an exponent corresponding to an interpretation of the final result as
414 // an integer.
Hans Boehm08e8f322015-04-21 13:18:38 -0700415 // We add an ellipsis on the left if the result was truncated.
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700416 // We add ellipses and exponents in a way that leaves most digits in the position they
417 // would have been in had we not done so.
418 // This minimizes jumps as a result of scrolling. Result is NOT internationalized,
Hans Boehm0b9806f2015-06-29 16:07:15 -0700419 // uses "E" for exponent.
Hans Boehm5e802f32015-06-22 17:18:52 -0700420 public String formatResult(String in, int precOffset, int maxDigs, boolean truncated,
421 boolean negative, int lastDisplayedOffset[], boolean forcePrecision) {
422 final int minusSpace = negative ? 1 : 0;
Hans Boehm3666e632015-07-27 18:33:12 -0700423 final int msdIndex = truncated ? -1 : getNaiveMsdIndexOf(in); // INVALID_MSD is OK.
Hans Boehm5e802f32015-06-22 17:18:52 -0700424 String result = in;
Hans Boehm73ecff22015-09-03 16:04:50 -0700425 if (truncated || (negative && result.charAt(0) != '-')) {
426 result = KeyMaps.ELLIPSIS + result.substring(1, result.length());
427 // Ellipsis may be removed again in the type(1) scientific notation case.
428 }
429 final int decIndex = result.indexOf('.');
Hans Boehm65a99a42016-02-03 18:16:07 -0800430 if (lastDisplayedOffset != null) {
431 lastDisplayedOffset[0] = precOffset;
432 }
Hans Boehm5e802f32015-06-22 17:18:52 -0700433 if ((decIndex == -1 || msdIndex != Evaluator.INVALID_MSD
434 && msdIndex - decIndex > MAX_LEADING_ZEROES + 1) && precOffset != -1) {
Hans Boehma0e45f32015-05-30 13:20:35 -0700435 // No decimal point displayed, and it's not just to the right of the last digit,
436 // or we should suppress leading zeroes.
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700437 // Add an exponent to let the user track which digits are currently displayed.
Hans Boehm5e802f32015-06-22 17:18:52 -0700438 // Start with type (2) exponent if we dropped no digits. -1 accounts for decimal point.
439 final int initExponent = precOffset > 0 ? -precOffset : -precOffset - 1;
440 int exponent = initExponent;
Hans Boehm08e8f322015-04-21 13:18:38 -0700441 boolean hasPoint = false;
Hans Boehm5e802f32015-06-22 17:18:52 -0700442 if (!truncated && msdIndex < maxDigs - 1
443 && result.length() - msdIndex + 1 + minusSpace
444 <= maxDigs + SCI_NOTATION_EXTRA) {
445 // Type (1) exponent computation and transformation:
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700446 // Leading digit is in display window. Use standard calculator scientific notation
447 // with one digit to the left of the decimal point. Insert decimal point and
448 // delete leading zeroes.
Hans Boehma0e45f32015-05-30 13:20:35 -0700449 // We try to keep leading digits roughly in position, and never
Hans Boehmf6dae112015-06-18 17:57:50 -0700450 // lengthen the result by more than SCI_NOTATION_EXTRA.
Hans Boehm5e802f32015-06-22 17:18:52 -0700451 final int resLen = result.length();
452 String fraction = result.substring(msdIndex + 1, resLen);
453 result = (negative ? "-" : "") + result.substring(msdIndex, msdIndex + 1)
454 + "." + fraction;
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700455 // Original exp was correct for decimal point at right of fraction.
456 // Adjust by length of fraction.
Hans Boehm5e802f32015-06-22 17:18:52 -0700457 exponent = initExponent + resLen - msdIndex - 1;
Hans Boehm08e8f322015-04-21 13:18:38 -0700458 hasPoint = true;
459 }
Hans Boehm73ecff22015-09-03 16:04:50 -0700460 // Exponent can't be zero.
461 // Actually add the exponent of either type:
462 if (!forcePrecision) {
463 int dropDigits; // Digits to drop to make room for exponent.
464 if (hasPoint) {
465 // Type (1) exponent.
466 // Drop digits even if there is room. Otherwise the scrolling gets jumpy.
467 dropDigits = expLen(exponent);
468 if (dropDigits >= result.length() - 1) {
469 // Jumpy is better than no mantissa. Probably impossible anyway.
470 dropDigits = Math.max(result.length() - 2, 0);
Hans Boehma0e45f32015-05-30 13:20:35 -0700471 }
Hans Boehm73ecff22015-09-03 16:04:50 -0700472 } else {
473 // Type (2) exponent.
474 // Exponent depends on the number of digits we drop, which depends on
475 // exponent ...
476 for (dropDigits = 2; expLen(initExponent + dropDigits) > dropDigits;
477 ++dropDigits) {}
478 exponent = initExponent + dropDigits;
479 if (precOffset - dropDigits > mLsdOffset) {
480 // This can happen if e.g. result = 10^40 + 10^10
481 // It turns out we would otherwise display ...10e9 because it takes
482 // the same amount of space as ...1e10 but shows one more digit.
483 // But we don't want to display a trailing zero, even if it's free.
484 ++dropDigits;
485 ++exponent;
486 }
Hans Boehm08e8f322015-04-21 13:18:38 -0700487 }
Hans Boehm73ecff22015-09-03 16:04:50 -0700488 result = result.substring(0, result.length() - dropDigits);
Hans Boehm65a99a42016-02-03 18:16:07 -0800489 if (lastDisplayedOffset != null) {
490 lastDisplayedOffset[0] -= dropDigits;
491 }
Hans Boehm73ecff22015-09-03 16:04:50 -0700492 }
493 result = result + "E" + Integer.toString(exponent);
Hans Boehm5e802f32015-06-22 17:18:52 -0700494 }
495 return result;
Hans Boehm08e8f322015-04-21 13:18:38 -0700496 }
497
Hans Boehmf6dae112015-06-18 17:57:50 -0700498 /**
499 * Get formatted, but not internationalized, result from mEvaluator.
Hans Boehm5e802f32015-06-22 17:18:52 -0700500 * @param precOffset requested position (1 = tenths) of last included digit.
Hans Boehmf6dae112015-06-18 17:57:50 -0700501 * @param maxSize Maximum number of characters (more or less) in result.
Hans Boehm5e802f32015-06-22 17:18:52 -0700502 * @param lastDisplayedOffset Zeroth entry is set to actual offset of last included digit,
Hans Boehm65a99a42016-02-03 18:16:07 -0800503 * after adjusting for exponent, etc. May be null.
Hans Boehmf6dae112015-06-18 17:57:50 -0700504 * @param forcePrecision Ensure that last included digit is at pos, at the expense
505 * of treating maxSize as a soft limit.
506 */
Hans Boehm5e802f32015-06-22 17:18:52 -0700507 private String getFormattedResult(int precOffset, int maxSize, int lastDisplayedOffset[],
Hans Boehmf6dae112015-06-18 17:57:50 -0700508 boolean forcePrecision) {
Hans Boehm08e8f322015-04-21 13:18:38 -0700509 final boolean truncated[] = new boolean[1];
510 final boolean negative[] = new boolean[1];
Hans Boehm5e802f32015-06-22 17:18:52 -0700511 final int requestedPrecOffset[] = {precOffset};
512 final String rawResult = mEvaluator.getString(requestedPrecOffset, mMaxCharOffset,
Hans Boehma0e45f32015-05-30 13:20:35 -0700513 maxSize, truncated, negative);
Hans Boehm5e802f32015-06-22 17:18:52 -0700514 return formatResult(rawResult, requestedPrecOffset[0], maxSize, truncated[0], negative[0],
515 lastDisplayedOffset, forcePrecision);
Hans Boehm08e8f322015-04-21 13:18:38 -0700516 }
517
Hans Boehm65a99a42016-02-03 18:16:07 -0800518 /**
519 * Return entire result (within reason) up to current displayed precision.
520 */
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700521 public String getFullText() {
Hans Boehm760a9dc2015-04-20 10:27:12 -0700522 if (!mValid) return "";
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700523 if (!mScrollable) return getText().toString();
Hans Boehm5e802f32015-06-22 17:18:52 -0700524 return KeyMaps.translateResult(getFormattedResult(mLastDisplayedOffset, MAX_COPY_SIZE,
Hans Boehm65a99a42016-02-03 18:16:07 -0800525 null, true));
Hans Boehm84614952014-11-25 18:46:17 -0800526 }
527
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700528 public boolean fullTextIsExact() {
Hans Boehmf6dae112015-06-18 17:57:50 -0700529 return !mScrollable
Hans Boehm5e802f32015-06-22 17:18:52 -0700530 || mMaxCharOffset == getCurrentCharOffset() && mMaxCharOffset != MAX_RIGHT_SCROLL;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700531 }
532
Hans Boehm61568a12015-05-18 18:25:41 -0700533 /**
Hans Boehm65a99a42016-02-03 18:16:07 -0800534 * Get entire result up to current displayed precision, or up to MAX_COPY_EXTRA additional
535 * digits, if it will lead to an exact result.
536 */
537 public String getFullCopyText() {
538 if (!mValid
539 || mLsdOffset == Integer.MAX_VALUE
540 || fullTextIsExact()
541 || mWholeLen > MAX_RECOMPUTE_DIGITS
542 || mWholeLen + mLsdOffset > MAX_RECOMPUTE_DIGITS
543 || mLsdOffset - mLastDisplayedOffset > MAX_COPY_EXTRA) {
544 return getFullText();
545 }
546 // It's reasonable to compute and copy the exact result instead.
547 final int nonNegLsdOffset = Math.max(0, mLsdOffset);
Hans Boehm995e5eb2016-02-08 11:03:01 -0800548 final String rawResult = mEvaluator.getResult().toStringTruncated(nonNegLsdOffset);
Hans Boehm65a99a42016-02-03 18:16:07 -0800549 final String formattedResult = formatResult(rawResult, nonNegLsdOffset, MAX_COPY_SIZE,
550 false, rawResult.charAt(0) == '-', null, true);
551 return KeyMaps.translateResult(formattedResult);
552 }
553
554 /**
Hans Boehm61568a12015-05-18 18:25:41 -0700555 * Return the maximum number of characters that will fit in the result display.
556 * May be called asynchronously from non-UI thread.
557 */
Hans Boehm84614952014-11-25 18:46:17 -0800558 int getMaxChars() {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700559 int result;
560 synchronized(mWidthLock) {
Justin Klaassen44595162015-05-28 17:55:20 -0700561 result = (int) Math.floor(mWidthConstraint / mCharWidth);
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700562 // We can apparently finish evaluating before onMeasure in CalculatorText has been
563 // called, in which case we get 0 or -1 as the width constraint.
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700564 }
Hans Boehm84614952014-11-25 18:46:17 -0800565 if (result <= 0) {
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700566 // Return something conservatively big, to force sufficient evaluation.
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700567 return MAX_WIDTH;
Hans Boehm84614952014-11-25 18:46:17 -0800568 } else {
Hans Boehm80018c82015-08-02 16:59:07 -0700569 return result;
Hans Boehm84614952014-11-25 18:46:17 -0800570 }
571 }
572
Hans Boehm61568a12015-05-18 18:25:41 -0700573 /**
Justin Klaassen44595162015-05-28 17:55:20 -0700574 * @return {@code true} if the currently displayed result is scrollable
Hans Boehm61568a12015-05-18 18:25:41 -0700575 */
Justin Klaassen44595162015-05-28 17:55:20 -0700576 public boolean isScrollable() {
577 return mScrollable;
Hans Boehm61568a12015-05-18 18:25:41 -0700578 }
579
Hans Boehm5e802f32015-06-22 17:18:52 -0700580 int getCurrentCharOffset() {
Hans Boehm013969e2015-04-13 20:29:47 -0700581 synchronized(mWidthLock) {
Hans Boehma0e45f32015-05-30 13:20:35 -0700582 return (int) Math.round(mCurrentPos / mCharWidth);
Hans Boehm013969e2015-04-13 20:29:47 -0700583 }
584 }
585
Hans Boehm84614952014-11-25 18:46:17 -0800586 void clear() {
Hans Boehm760a9dc2015-04-20 10:27:12 -0700587 mValid = false;
Hans Boehm1176f232015-05-11 16:26:03 -0700588 mScrollable = false;
Hans Boehm84614952014-11-25 18:46:17 -0800589 setText("");
590 }
591
592 void redisplay() {
Hans Boehm5e802f32015-06-22 17:18:52 -0700593 int currentCharOffset = getCurrentCharOffset();
Hans Boehm84614952014-11-25 18:46:17 -0800594 int maxChars = getMaxChars();
Hans Boehm5e802f32015-06-22 17:18:52 -0700595 int lastDisplayedOffset[] = new int[1];
596 String result = getFormattedResult(currentCharOffset, maxChars, lastDisplayedOffset, false);
Hans Boehm0b9806f2015-06-29 16:07:15 -0700597 int expIndex = result.indexOf('E');
Hans Boehm013969e2015-04-13 20:29:47 -0700598 result = KeyMaps.translateResult(result);
Hans Boehm5e802f32015-06-22 17:18:52 -0700599 if (expIndex > 0 && result.indexOf('.') == -1) {
Hans Boehm84614952014-11-25 18:46:17 -0800600 // Gray out exponent if used as position indicator
601 SpannableString formattedResult = new SpannableString(result);
Hans Boehm5e802f32015-06-22 17:18:52 -0700602 formattedResult.setSpan(mExponentColorSpan, expIndex, result.length(),
Hans Boehm84614952014-11-25 18:46:17 -0800603 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
604 setText(formattedResult);
605 } else {
606 setText(result);
607 }
Hans Boehm5e802f32015-06-22 17:18:52 -0700608 mLastDisplayedOffset = lastDisplayedOffset[0];
Hans Boehm760a9dc2015-04-20 10:27:12 -0700609 mValid = true;
Hans Boehm84614952014-11-25 18:46:17 -0800610 }
611
612 @Override
613 public void computeScroll() {
614 if (!mScrollable) return;
615 if (mScroller.computeScrollOffset()) {
616 mCurrentPos = mScroller.getCurrX();
617 if (mCurrentPos != mLastPos) {
618 mLastPos = mCurrentPos;
619 redisplay();
620 }
621 if (!mScroller.isFinished()) {
Justin Klaassen44595162015-05-28 17:55:20 -0700622 postInvalidateOnAnimation();
Hans Boehm84614952014-11-25 18:46:17 -0800623 }
624 }
625 }
626
Chenjie Yu3937b652016-06-01 23:14:26 -0700627 /**
628 * Use ActionMode for copy support on M and higher.
629 */
630 @TargetApi(Build.VERSION_CODES.M)
631 private void setupActionMode() {
632 mCopyActionModeCallback = new ActionMode.Callback2() {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700633
Chenjie Yu3937b652016-06-01 23:14:26 -0700634 @Override
635 public boolean onCreateActionMode(ActionMode mode, Menu menu) {
636 final MenuInflater inflater = mode.getMenuInflater();
637 return createCopyMenu(inflater, menu);
638 }
Hans Boehm7f83e362015-06-10 15:41:04 -0700639
Chenjie Yu3937b652016-06-01 23:14:26 -0700640 @Override
641 public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
642 return false; // Return false if nothing is done
643 }
Hans Boehm7f83e362015-06-10 15:41:04 -0700644
Chenjie Yu3937b652016-06-01 23:14:26 -0700645 @Override
646 public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
647 if (onMenuItemClick(item)) {
Hans Boehm65a99a42016-02-03 18:16:07 -0800648 mode.finish();
649 return true;
Chenjie Yu3937b652016-06-01 23:14:26 -0700650 } else {
651 return false;
Hans Boehm65a99a42016-02-03 18:16:07 -0800652 }
Chenjie Yu3937b652016-06-01 23:14:26 -0700653 }
654
655 @Override
656 public void onDestroyActionMode(ActionMode mode) {
657 unhighlightResult();
658 mActionMode = null;
659 }
660
661 @Override
662 public void onGetContentRect(ActionMode mode, View view, Rect outRect) {
663 super.onGetContentRect(mode, view, outRect);
664
665 outRect.left += view.getPaddingLeft();
666 outRect.top += view.getPaddingTop();
667 outRect.right -= view.getPaddingRight();
668 outRect.bottom -= view.getPaddingBottom();
669 final int width = (int) Layout.getDesiredWidth(getText(), getPaint());
670 if (width < outRect.width()) {
671 outRect.left = outRect.right - width;
672 }
673
674 if (!BuildCompat.isAtLeastN()) {
675 // The CAB (prior to N) only takes the translation of a view into account, so
676 // if a scale is applied to the view then the offset outRect will end up being
677 // positioned incorrectly. We workaround that limitation by manually applying
678 // the scale to the outRect, which the CAB will then offset to the correct
679 // position.
680 final float scaleX = view.getScaleX();
681 final float scaleY = view.getScaleY();
682 outRect.left *= scaleX;
683 outRect.right *= scaleX;
684 outRect.top *= scaleY;
685 outRect.bottom *= scaleY;
686 }
687 }
688 };
689 setOnLongClickListener(new View.OnLongClickListener() {
690 @Override
691 public boolean onLongClick(View v) {
692 if (mValid) {
693 mActionMode = startActionMode(mCopyActionModeCallback,
694 ActionMode.TYPE_FLOATING);
695 return true;
696 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700697 return false;
698 }
Chenjie Yu3937b652016-06-01 23:14:26 -0700699 });
700 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700701
Chenjie Yu3937b652016-06-01 23:14:26 -0700702 /**
703 * Use ContextMenu for copy support on L and lower.
704 */
705 private void setupContextMenu() {
706 setOnCreateContextMenuListener(new OnCreateContextMenuListener() {
707 @Override
708 public void onCreateContextMenu(ContextMenu contextMenu, View view,
709 ContextMenu.ContextMenuInfo contextMenuInfo) {
710 final MenuInflater inflater = new MenuInflater(getContext());
711 createCopyMenu(inflater, contextMenu);
712 mContextMenu = contextMenu;
713 for(int i = 0; i < contextMenu.size(); i ++) {
714 contextMenu.getItem(i).setOnMenuItemClickListener(CalculatorResult.this);
715 }
Hans Boehm7f83e362015-06-10 15:41:04 -0700716 }
Chenjie Yu3937b652016-06-01 23:14:26 -0700717 });
718 setOnLongClickListener(new View.OnLongClickListener() {
719 @Override
720 public boolean onLongClick(View v) {
721 if (mValid) {
722 return showContextMenu();
723 }
724 return false;
Justin Klaassenf1b61f42016-04-27 16:00:11 -0700725 }
Chenjie Yu3937b652016-06-01 23:14:26 -0700726 });
727 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700728
Chenjie Yu3937b652016-06-01 23:14:26 -0700729 private boolean createCopyMenu(MenuInflater inflater, Menu menu) {
730 inflater.inflate(R.menu.copy, menu);
731 highlightResult();
732 return true;
733 }
734
735 public boolean stopActionModeOrContextMenu() {
Hans Boehm1176f232015-05-11 16:26:03 -0700736 if (mActionMode != null) {
737 mActionMode.finish();
738 return true;
739 }
Chenjie Yu3937b652016-06-01 23:14:26 -0700740 if (mContextMenu != null) {
741 unhighlightResult();
742 mContextMenu.close();
743 return true;
744 }
Hans Boehm1176f232015-05-11 16:26:03 -0700745 return false;
746 }
747
Chenjie Yu3937b652016-06-01 23:14:26 -0700748 private void highlightResult() {
749 final Spannable text = (Spannable) getText();
750 text.setSpan(mHighlightSpan, 0, text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
751 }
752
753 private void unhighlightResult() {
754 final Spannable text = (Spannable) getText();
755 text.removeSpan(mHighlightSpan);
756 }
757
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700758 private void setPrimaryClip(ClipData clip) {
759 ClipboardManager clipboard = (ClipboardManager) getContext().
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700760 getSystemService(Context.CLIPBOARD_SERVICE);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700761 clipboard.setPrimaryClip(clip);
762 }
763
764 private void copyContent() {
Hans Boehm65a99a42016-02-03 18:16:07 -0800765 final CharSequence text = getFullCopyText();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700766 ClipboardManager clipboard =
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700767 (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
768 // We include a tag URI, to allow us to recognize our own results and handle them
769 // specially.
770 ClipData.Item newItem = new ClipData.Item(text, null, mEvaluator.capture());
771 String[] mimeTypes = new String[] {ClipDescription.MIMETYPE_TEXT_PLAIN};
772 ClipData cd = new ClipData("calculator result", mimeTypes, newItem);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700773 clipboard.setPrimaryClip(cd);
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700774 Toast.makeText(getContext(), R.string.text_copied_toast, Toast.LENGTH_SHORT).show();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700775 }
776
Chenjie Yu3937b652016-06-01 23:14:26 -0700777 @Override
778 public boolean onMenuItemClick(MenuItem item) {
779 switch (item.getItemId()) {
780 case R.id.menu_copy:
781 if (mEvaluator.reevaluationInProgress()) {
782 // Refuse to copy placeholder characters.
783 return false;
784 } else {
785 copyContent();
786 unhighlightResult();
787 return true;
788 }
789 default:
790 return false;
791 }
792 }
Hans Boehm84614952014-11-25 18:46:17 -0800793}