blob: de09ff65e012139a4813c66db43fd7c6523e918c [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
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070019import android.content.ClipData;
20import android.content.ClipDescription;
Justin Klaassen44595162015-05-28 17:55:20 -070021import android.content.ClipboardManager;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070022import android.content.Context;
Justin Klaassen44595162015-05-28 17:55:20 -070023import android.text.Layout;
Hans Boehm84614952014-11-25 18:46:17 -080024import android.text.SpannableString;
Hans Boehm1176f232015-05-11 16:26:03 -070025import android.text.Spanned;
Justin Klaassen44595162015-05-28 17:55:20 -070026import android.text.TextPaint;
Hans Boehm84614952014-11-25 18:46:17 -080027import android.text.style.ForegroundColorSpan;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070028import android.util.AttributeSet;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070029import android.view.ActionMode;
30import android.view.GestureDetector;
31import android.view.Menu;
32import android.view.MenuInflater;
33import android.view.MenuItem;
34import android.view.MotionEvent;
35import android.view.View;
Justin Klaassen44595162015-05-28 17:55:20 -070036import android.widget.OverScroller;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070037import android.widget.Toast;
Hans Boehm84614952014-11-25 18:46:17 -080038
Hans Boehm84614952014-11-25 18:46:17 -080039// A text widget that is "infinitely" scrollable to the right,
40// and obtains the text to display via a callback to Logic.
Justin Klaassen44595162015-05-28 17:55:20 -070041public class CalculatorResult extends AlignedTextView {
Hans Boehm61568a12015-05-18 18:25:41 -070042 static final int MAX_RIGHT_SCROLL = 10000000;
Hans Boehm08e8f322015-04-21 13:18:38 -070043 static final int INVALID = MAX_RIGHT_SCROLL + 10000;
Hans Boehm84614952014-11-25 18:46:17 -080044 // A larger value is unlikely to avoid running out of space
45 final OverScroller mScroller;
46 final GestureDetector mGestureDetector;
47 class MyTouchListener implements View.OnTouchListener {
48 @Override
49 public boolean onTouch(View v, MotionEvent event) {
Justin Klaassen44595162015-05-28 17:55:20 -070050 return mGestureDetector.onTouchEvent(event);
Hans Boehm84614952014-11-25 18:46:17 -080051 }
52 }
53 final MyTouchListener mTouchListener = new MyTouchListener();
54 private Evaluator mEvaluator;
55 private boolean mScrollable = false;
56 // A scrollable result is currently displayed.
Hans Boehm760a9dc2015-04-20 10:27:12 -070057 private boolean mValid = false;
Hans Boehmc01cd7f2015-05-12 18:32:19 -070058 // The result holds something valid; either a a number or an error
59 // message.
60 private int mCurrentPos;// Position of right of display relative to decimal point, in pixels.
61 // Large positive values mean the decimal point is scrolled off the
62 // left of the display. Zero means decimal point is barely displayed
63 // on the right.
Hans Boehm61568a12015-05-18 18:25:41 -070064 private int mLastPos; // Position already reflected in display. Pixels.
65 private int mMinPos; // Minimum position before all digits disappear off the right. Pixels.
66 private int mMaxPos; // Maximum position before we start displaying the infinite
67 // sequence of trailing zeroes on the right. Pixels.
Hans Boehma0e45f32015-05-30 13:20:35 -070068 private int mMaxCharPos; // The same, but in characters.
69 private int mLsd; // Position of least-significant digit in result
70 // (1 = tenths, -1 = tens), or Integer.MAX_VALUE.
Hans Boehmf6dae112015-06-18 17:57:50 -070071 private int mLastDisplayedDigit; // Position of last digit actually displayed after adding
72 // exponent.
Justin Klaassen44595162015-05-28 17:55:20 -070073 private final Object mWidthLock = new Object();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070074 // Protects the next two fields.
75 private int mWidthConstraint = -1;
Hans Boehma0e45f32015-05-30 13:20:35 -070076 // Our total width in pixels minus space for ellipsis.
Justin Klaassen44595162015-05-28 17:55:20 -070077 private float mCharWidth = 1;
Hans Boehmc01cd7f2015-05-12 18:32:19 -070078 // Maximum character width. For now we pretend that all characters
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070079 // have this width.
Hans Boehmc01cd7f2015-05-12 18:32:19 -070080 // TODO: We're not really using a fixed width font. But it appears
81 // to be close enough for the characters we use that the difference
82 // is not noticeable.
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070083 private static final int MAX_WIDTH = 100;
84 // Maximum number of digits displayed
Hans Boehm50ed3202015-06-09 14:35:49 -070085 public static final int MAX_LEADING_ZEROES = 6;
Hans Boehma0e45f32015-05-30 13:20:35 -070086 // Maximum number of leading zeroes after decimal point before we
87 // switch to scientific notation with negative exponent.
Hans Boehm50ed3202015-06-09 14:35:49 -070088 public static final int MAX_TRAILING_ZEROES = 6;
Hans Boehma0e45f32015-05-30 13:20:35 -070089 // Maximum number of trailing zeroes before the decimal point before
90 // we switch to scientific notation with positive exponent.
91 private static final int SCI_NOTATION_EXTRA = 1;
92 // Extra digits for standard scientific notation. In this case we
93 // have a deecimal point and no ellipsis.
Hans Boehm1176f232015-05-11 16:26:03 -070094 private ActionMode mActionMode;
95 private final ForegroundColorSpan mExponentColorSpan;
Hans Boehm84614952014-11-25 18:46:17 -080096
97 public CalculatorResult(Context context, AttributeSet attrs) {
98 super(context, attrs);
99 mScroller = new OverScroller(context);
100 mGestureDetector = new GestureDetector(context,
101 new GestureDetector.SimpleOnGestureListener() {
102 @Override
Justin Klaassend48b7562015-04-16 16:51:38 -0700103 public boolean onDown(MotionEvent e) {
104 return true;
105 }
106 @Override
Hans Boehm84614952014-11-25 18:46:17 -0800107 public boolean onFling(MotionEvent e1, MotionEvent e2,
108 float velocityX, float velocityY) {
109 if (!mScroller.isFinished()) {
110 mCurrentPos = mScroller.getFinalX();
111 }
112 mScroller.forceFinished(true);
Hans Boehm1176f232015-05-11 16:26:03 -0700113 stopActionMode();
Hans Boehmfbcef702015-04-27 18:07:47 -0700114 CalculatorResult.this.cancelLongPress();
115 // Ignore scrolls of error string, etc.
116 if (!mScrollable) return true;
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700117 mScroller.fling(mCurrentPos, 0, - (int) velocityX, 0 /* horizontal only */,
Hans Boehm61568a12015-05-18 18:25:41 -0700118 mMinPos, mMaxPos, 0, 0);
Justin Klaassen44595162015-05-28 17:55:20 -0700119 postInvalidateOnAnimation();
Hans Boehm84614952014-11-25 18:46:17 -0800120 return true;
121 }
122 @Override
123 public boolean onScroll(MotionEvent e1, MotionEvent e2,
124 float distanceX, float distanceY) {
Hans Boehm61568a12015-05-18 18:25:41 -0700125 int distance = (int)distanceX;
Hans Boehm84614952014-11-25 18:46:17 -0800126 if (!mScroller.isFinished()) {
127 mCurrentPos = mScroller.getFinalX();
128 }
129 mScroller.forceFinished(true);
Hans Boehm1176f232015-05-11 16:26:03 -0700130 stopActionMode();
Hans Boehm84614952014-11-25 18:46:17 -0800131 CalculatorResult.this.cancelLongPress();
132 if (!mScrollable) return true;
Hans Boehm61568a12015-05-18 18:25:41 -0700133 if (mCurrentPos + distance < mMinPos) {
134 distance = mMinPos - mCurrentPos;
135 } else if (mCurrentPos + distance > mMaxPos) {
136 distance = mMaxPos - mCurrentPos;
137 }
Hans Boehm84614952014-11-25 18:46:17 -0800138 int duration = (int)(e2.getEventTime() - e1.getEventTime());
139 if (duration < 1 || duration > 100) duration = 10;
Hans Boehm61568a12015-05-18 18:25:41 -0700140 mScroller.startScroll(mCurrentPos, 0, distance, 0, (int)duration);
Justin Klaassen44595162015-05-28 17:55:20 -0700141 postInvalidateOnAnimation();
Hans Boehm84614952014-11-25 18:46:17 -0800142 return true;
143 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700144 @Override
145 public void onLongPress(MotionEvent e) {
Hans Boehm1176f232015-05-11 16:26:03 -0700146 if (mValid) {
147 mActionMode = startActionMode(mCopyActionModeCallback,
148 ActionMode.TYPE_FLOATING);
149 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700150 }
Hans Boehm84614952014-11-25 18:46:17 -0800151 });
152 setOnTouchListener(mTouchListener);
153 setHorizontallyScrolling(false); // do it ourselves
154 setCursorVisible(false);
Hans Boehm1176f232015-05-11 16:26:03 -0700155 mExponentColorSpan = new ForegroundColorSpan(
156 context.getColor(R.color.display_result_exponent_text_color));
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700157
158 // Copy ActionMode is triggered explicitly, not through
159 // setCustomSelectionActionModeCallback.
Hans Boehm84614952014-11-25 18:46:17 -0800160 }
161
162 void setEvaluator(Evaluator evaluator) {
163 mEvaluator = evaluator;
164 }
165
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700166 @Override
167 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
168 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
169
Justin Klaassen44595162015-05-28 17:55:20 -0700170 final TextPaint paint = getPaint();
171 final int newWidthConstraint = MeasureSpec.getSize(widthMeasureSpec)
172 - (getPaddingLeft() + getPaddingRight())
173 - (int) Math.ceil(Layout.getDesiredWidth(KeyMaps.ELLIPSIS, paint));
174 final float newCharWidth = Layout.getDesiredWidth("\u2007", paint);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700175 synchronized(mWidthLock) {
Hans Boehm013969e2015-04-13 20:29:47 -0700176 mWidthConstraint = newWidthConstraint;
177 mCharWidth = newCharWidth;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700178 }
179 }
180
Hans Boehma0e45f32015-05-30 13:20:35 -0700181 // Return the length of the exponent representation for the given exponent, in
182 // characters.
183 private final int expLen(int exp) {
184 if (exp == 0) return 0;
185 return (int)Math.ceil(Math.log10(Math.abs((double)exp))) + (exp >= 0 ? 1 : 2);
Hans Boehm61568a12015-05-18 18:25:41 -0700186 }
187
Hans Boehma0e45f32015-05-30 13:20:35 -0700188 /**
189 * Initiate display of a new result.
190 * The parameters specify various properties of the result.
191 * @param initPrec Initial display precision computed by evaluator. (1 = tenths digit)
192 * @param msd Position of most significant digit. Offset from left of string.
193 Evaluator.INVALID_MSD if unknown.
194 * @param leastDigPos Position of least significant digit (1 = tenths digit)
195 * or Integer.MAX_VALUE.
196 * @param truncatedWholePart Result up to but not including decimal point.
197 Currently we only use the length.
198 */
199 void displayResult(int initPrec, int msd, int leastDigPos, String truncatedWholePart) {
200 initPositions(initPrec, msd, leastDigPos, truncatedWholePart);
Hans Boehm84614952014-11-25 18:46:17 -0800201 redisplay();
202 }
203
Hans Boehma0e45f32015-05-30 13:20:35 -0700204 /**
205 * Set up scroll bounds and determine whether the result is scrollable, based on the
206 * supplied information about the result.
207 * This is unfortunately complicated because we need to predict whether trailing digits
208 * will eventually be replaced by an exponent.
209 * Just appending the exponent during formatting would be simpler, but would produce
210 * jumpier results during transitions.
211 */
212 private void initPositions(int initPrec, int msd, int leastDigPos, String truncatedWholePart) {
213 float charWidth;
214 int maxChars = getMaxChars();
215 mLastPos = INVALID;
216 mLsd = leastDigPos;
217 synchronized(mWidthLock) {
218 charWidth = mCharWidth;
219 }
220 mCurrentPos = mMinPos = (int) Math.round(initPrec * charWidth);
221 // Prevent scrolling past initial position, which is calculated to show leading digits.
222 if (msd == Evaluator.INVALID_MSD) {
223 // Possible zero value
224 if (leastDigPos == Integer.MIN_VALUE) {
225 // Definite zero value.
226 mMaxPos = mMinPos;
227 mMaxCharPos = (int) Math.round(mMaxPos/charWidth);
228 mScrollable = false;
229 } else {
230 // May be very small nonzero value. Allow user to find out.
231 mMaxPos = mMaxCharPos = MAX_RIGHT_SCROLL;
232 mScrollable = true;
233 }
234 return;
235 }
236 int wholeLen = truncatedWholePart.length();
237 int negative = truncatedWholePart.charAt(0) == '-' ? 1 : 0;
238 boolean adjustedForExp = false; // Adjusted for normal exponent.
239 if (msd > wholeLen && msd <= wholeLen + 3) {
240 // Avoid tiny negative exponent; pretend msd is just to the right of decimal point.
241 msd = wholeLen - 1;
242 }
243 int minCharPos = msd - negative - wholeLen;
244 // Position of leftmost significant digit relative to dec. point.
245 // Usually negative.
246 mMaxCharPos = MAX_RIGHT_SCROLL; // How far does it make sense to scroll right?
247 // If msd is left of decimal point should logically be
248 // mMinPos = - (int) Math.ceil(getPaint().measureText(truncatedWholePart)), but
249 // we eventually translate to a character position by dividing by mCharWidth.
250 // To avoid rounding issues, we use the analogous computation here.
251 if (minCharPos > -1 && minCharPos < MAX_LEADING_ZEROES + 2) {
252 // Small number of leading zeroes, avoid scientific notation.
253 minCharPos = -1;
254 }
255 if (leastDigPos < MAX_RIGHT_SCROLL) {
256 mMaxCharPos = leastDigPos;
257 if (mMaxCharPos < -1 && mMaxCharPos > -(MAX_TRAILING_ZEROES + 2)) {
258 mMaxCharPos = -1;
259 }
260 // leastDigPos is positive or negative, never 0.
261 if (mMaxCharPos < -1) {
262 // Number entirely to left of decimal point.
263 // We'll need a positive exponent or displayed zeros to display entire number.
264 mMaxCharPos = Math.min(-1, mMaxCharPos + expLen(-minCharPos - 1));
265 if (mMaxCharPos >= -1) {
266 // Unlikely; huge exponent.
267 mMaxCharPos = -1;
268 } else {
269 adjustedForExp = true;
270 }
271 } else if (minCharPos > -1 || mMaxCharPos >= maxChars) {
272 // Number either entirely to the right of decimal point, or decimal point not
273 // visible when scrolled to the right.
274 // We will need an exponent when looking at the rightmost digit.
275 // Allow additional scrolling to make room.
276 mMaxCharPos += expLen(-(minCharPos + 1));
277 adjustedForExp = true;
278 // Assumed an exponent for standard scientific notation for now.
279 // Adjusted below if necessary.
280 }
281 mScrollable = (mMaxCharPos - minCharPos + negative >= maxChars);
282 if (mScrollable) {
283 if (adjustedForExp) {
284 // We may need a slightly larger negative exponent while scrolling.
285 mMaxCharPos += expLen(-leastDigPos) - expLen(-(minCharPos + 1));
286 }
287 }
288 mMaxPos = Math.min((int) Math.round(mMaxCharPos * charWidth), MAX_RIGHT_SCROLL);
289 if (!mScrollable) {
290 // Position the number consistently with our assumptions to make sure it
291 // actually fits.
292 mCurrentPos = mMaxPos;
293 }
294 } else {
295 mMaxPos = mMaxCharPos = MAX_RIGHT_SCROLL;
296 mScrollable = true;
297 }
298 }
299
Hans Boehm84614952014-11-25 18:46:17 -0800300 void displayError(int resourceId) {
Hans Boehm760a9dc2015-04-20 10:27:12 -0700301 mValid = true;
Hans Boehm84614952014-11-25 18:46:17 -0800302 mScrollable = false;
303 setText(resourceId);
304 }
305
Hans Boehm013969e2015-04-13 20:29:47 -0700306 private final int MAX_COPY_SIZE = 1000000;
307
Hans Boehma0e45f32015-05-30 13:20:35 -0700308 /*
309 * Return the most significant digit position in the given string or Evaluator.INVALID_MSD.
310 * Unlike Evaluator.getMsdPos, we treat a final 1 as significant.
311 */
312 public static int getNaiveMsdPos(String s) {
313 int len = s.length();
314 int nonzeroPos = -1;
315 for (int i = 0; i < len; ++i) {
316 char c = s.charAt(i);
317 if (c != '-' && c != '.' && c != '0') {
318 return i;
319 }
320 }
321 return Evaluator.INVALID_MSD;
322 }
323
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700324 // Format a result returned by Evaluator.getString() into a single line containing ellipses
Hans Boehma0e45f32015-05-30 13:20:35 -0700325 // (if appropriate) and an exponent (if appropriate). prec is the value that was passed to
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700326 // getString and thus identifies the significance of the rightmost digit.
Hans Boehma0e45f32015-05-30 13:20:35 -0700327 // A value of 1 means the rightmost digits corresponds to tenths.
328 // maxDigs is the maximum number of characters in the result.
Hans Boehmf6dae112015-06-18 17:57:50 -0700329 // We set lastDisplayedDigit[0] to the position of the last digit actually appearing in
330 // the display.
331 // If forcePrecision is true, we make sure that the last displayed digit corresponds to
332 // prec, and allow maxDigs to be exceeded in assing the exponent.
Hans Boehm08e8f322015-04-21 13:18:38 -0700333 // We add two distinct kinds of exponents:
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700334 // 1) If the final result contains the leading digit we use standard scientific notation.
335 // 2) If not, we add an exponent corresponding to an interpretation of the final result as
336 // an integer.
Hans Boehm08e8f322015-04-21 13:18:38 -0700337 // We add an ellipsis on the left if the result was truncated.
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700338 // We add ellipses and exponents in a way that leaves most digits in the position they
339 // would have been in had we not done so.
340 // This minimizes jumps as a result of scrolling. Result is NOT internationalized,
341 // uses "e" for exponent.
Hans Boehmf6dae112015-06-18 17:57:50 -0700342 public String formatResult(String res, int prec, int maxDigs, boolean truncated,
343 boolean negative, int lastDisplayedDigit[], boolean forcePrecision) {
Hans Boehma0e45f32015-05-30 13:20:35 -0700344 int msd; // Position of most significant digit in res or indication its outside res.
345 int minusSpace = negative ? 1 : 0;
Hans Boehm08e8f322015-04-21 13:18:38 -0700346 if (truncated) {
347 res = KeyMaps.ELLIPSIS + res.substring(1, res.length());
Hans Boehma0e45f32015-05-30 13:20:35 -0700348 msd = -1;
349 } else {
350 msd = getNaiveMsdPos(res); // INVALID_MSD is OK and is treated as large.
Hans Boehm08e8f322015-04-21 13:18:38 -0700351 }
352 int decIndex = res.indexOf('.');
353 int resLen = res.length();
Hans Boehmf6dae112015-06-18 17:57:50 -0700354 lastDisplayedDigit[0] = prec;
Hans Boehma0e45f32015-05-30 13:20:35 -0700355 if ((decIndex == -1 || msd != Evaluator.INVALID_MSD
356 && msd - decIndex > MAX_LEADING_ZEROES + 1) && prec != -1) {
357 // No decimal point displayed, and it's not just to the right of the last digit,
358 // or we should suppress leading zeroes.
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700359 // Add an exponent to let the user track which digits are currently displayed.
360 // This is a bit tricky, since the number of displayed digits affects the displayed
361 // exponent, which can affect the room we have for mantissa digits. We occasionally
362 // display one digit too few. This is sometimes unavoidable, but we could
Hans Boehm08e8f322015-04-21 13:18:38 -0700363 // avoid it in more cases.
Hans Boehma0e45f32015-05-30 13:20:35 -0700364 int exp = prec > 0 ? -prec : -prec - 1;
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700365 // Can be used as TYPE (2) EXPONENT. -1 accounts for decimal point.
Hans Boehm08e8f322015-04-21 13:18:38 -0700366 boolean hasPoint = false;
Hans Boehma0e45f32015-05-30 13:20:35 -0700367 if (msd < maxDigs - 1 && msd >= 0 &&
368 resLen - msd + 1 /* dec. pt. */ + minusSpace <= maxDigs + SCI_NOTATION_EXTRA) {
Hans Boehm08e8f322015-04-21 13:18:38 -0700369 // TYPE (1) EXPONENT computation and transformation:
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700370 // Leading digit is in display window. Use standard calculator scientific notation
371 // with one digit to the left of the decimal point. Insert decimal point and
372 // delete leading zeroes.
Hans Boehma0e45f32015-05-30 13:20:35 -0700373 // We try to keep leading digits roughly in position, and never
Hans Boehmf6dae112015-06-18 17:57:50 -0700374 // lengthen the result by more than SCI_NOTATION_EXTRA.
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700375 String fraction = res.substring(msd + 1, resLen);
Hans Boehma0e45f32015-05-30 13:20:35 -0700376 res = (negative ? "-" : "") + res.substring(msd, msd + 1) + "." + fraction;
Hans Boehm08e8f322015-04-21 13:18:38 -0700377 exp += resLen - msd - 1;
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700378 // Original exp was correct for decimal point at right of fraction.
379 // Adjust by length of fraction.
Hans Boehm08e8f322015-04-21 13:18:38 -0700380 resLen = res.length();
381 hasPoint = true;
382 }
383 if (exp != 0 || truncated) {
384 // Actually add the exponent of either type:
385 String expAsString = Integer.toString(exp);
386 int expDigits = expAsString.length();
Hans Boehmf6dae112015-06-18 17:57:50 -0700387 if (!forcePrecision) {
388 int dropDigits = expDigits + 1;
389 // Drop digits even if there is room. Otherwise the scrolling gets jumpy.
390 if (dropDigits >= resLen - 1) {
391 dropDigits = Math.max(resLen - 2, 0);
392 // Jumpy is better than no mantissa. Probably impossible anyway.
Hans Boehm08e8f322015-04-21 13:18:38 -0700393 }
Hans Boehmf6dae112015-06-18 17:57:50 -0700394 if (!hasPoint) {
395 // Special handling for TYPE(2) EXPONENT:
396 exp += dropDigits;
Hans Boehma0e45f32015-05-30 13:20:35 -0700397 expAsString = Integer.toString(exp);
Hans Boehmf6dae112015-06-18 17:57:50 -0700398 // Adjust for digits we are about to drop to drop to make room for exponent.
399 // This can affect the room we have for the mantissa. We adjust only for
400 // positive exponents, when it could otherwise result in a truncated
401 // displayed result.
402 if (exp > 0 && expAsString.length() > expDigits) {
403 // ++expDigits; (dead code)
404 ++dropDigits;
405 ++exp;
406 expAsString = Integer.toString(exp);
407 // This cannot increase the length a second time.
408 }
409 if (prec - dropDigits > mLsd) {
410 // This can happen if e.g. result = 10^40 + 10^10
411 // It turns out we would otherwise display ...10e9 because it takes
412 // the same amount of space as ...1e10 but shows one more digit.
413 // But we don't want to display a trailing zero, even if it's free.
414 ++dropDigits;
415 ++exp;
416 expAsString = Integer.toString(exp);
417 }
Hans Boehma0e45f32015-05-30 13:20:35 -0700418 }
Hans Boehmf6dae112015-06-18 17:57:50 -0700419 res = res.substring(0, resLen - dropDigits);
420 lastDisplayedDigit[0] -= dropDigits;
Hans Boehm08e8f322015-04-21 13:18:38 -0700421 }
Hans Boehm08e8f322015-04-21 13:18:38 -0700422 res = res + "e" + expAsString;
423 } // else don't add zero exponent
424 }
425 return res;
426 }
427
Hans Boehmf6dae112015-06-18 17:57:50 -0700428 /**
429 * Get formatted, but not internationalized, result from mEvaluator.
430 * @param pos requested position (1 = tenths) of last included digit.
431 * @param maxSize Maximum number of characters (more or less) in result.
432 * @param lastDisplayedPrec Zeroth entry is set to actual position of last included digit,
433 * after adjusting for exponent, etc.
434 * @param forcePrecision Ensure that last included digit is at pos, at the expense
435 * of treating maxSize as a soft limit.
436 */
437 private String getFormattedResult(int pos, int maxSize, int lastDisplayedDigit[],
438 boolean forcePrecision) {
Hans Boehm08e8f322015-04-21 13:18:38 -0700439 final boolean truncated[] = new boolean[1];
440 final boolean negative[] = new boolean[1];
441 final int requested_prec[] = {pos};
Hans Boehma0e45f32015-05-30 13:20:35 -0700442 final String raw_res = mEvaluator.getString(requested_prec, mMaxCharPos,
443 maxSize, truncated, negative);
Hans Boehmf6dae112015-06-18 17:57:50 -0700444 return formatResult(raw_res, requested_prec[0], maxSize, truncated[0], negative[0],
445 lastDisplayedDigit, forcePrecision);
Hans Boehm08e8f322015-04-21 13:18:38 -0700446 }
447
Hans Boehm84614952014-11-25 18:46:17 -0800448 // Return entire result (within reason) up to current displayed precision.
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700449 public String getFullText() {
Hans Boehm760a9dc2015-04-20 10:27:12 -0700450 if (!mValid) return "";
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700451 if (!mScrollable) return getText().toString();
Hans Boehm013969e2015-04-13 20:29:47 -0700452 int currentCharPos = getCurrentCharPos();
Hans Boehmf6dae112015-06-18 17:57:50 -0700453 int unused[] = new int[1];
454 return KeyMaps.translateResult(getFormattedResult(mLastDisplayedDigit, MAX_COPY_SIZE,
455 unused, true));
Hans Boehm84614952014-11-25 18:46:17 -0800456 }
457
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700458 public boolean fullTextIsExact() {
Hans Boehmf6dae112015-06-18 17:57:50 -0700459 return !mScrollable
460 || mMaxCharPos == getCurrentCharPos() && mMaxCharPos != MAX_RIGHT_SCROLL;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700461 }
462
Hans Boehm61568a12015-05-18 18:25:41 -0700463 /**
464 * Return the maximum number of characters that will fit in the result display.
465 * May be called asynchronously from non-UI thread.
466 */
Hans Boehm84614952014-11-25 18:46:17 -0800467 int getMaxChars() {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700468 int result;
469 synchronized(mWidthLock) {
Justin Klaassen44595162015-05-28 17:55:20 -0700470 result = (int) Math.floor(mWidthConstraint / mCharWidth);
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700471 // We can apparently finish evaluating before onMeasure in CalculatorText has been
472 // called, in which case we get 0 or -1 as the width constraint.
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700473 }
Hans Boehm84614952014-11-25 18:46:17 -0800474 if (result <= 0) {
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700475 // Return something conservatively big, to force sufficient evaluation.
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700476 return MAX_WIDTH;
Hans Boehm84614952014-11-25 18:46:17 -0800477 } else {
Justin Klaassen44595162015-05-28 17:55:20 -0700478 // Always allow for the ellipsis character which already accounted for in the width
479 // constraint.
480 return result + 1;
Hans Boehm84614952014-11-25 18:46:17 -0800481 }
482 }
483
Hans Boehm61568a12015-05-18 18:25:41 -0700484 /**
Justin Klaassen44595162015-05-28 17:55:20 -0700485 * @return {@code true} if the currently displayed result is scrollable
Hans Boehm61568a12015-05-18 18:25:41 -0700486 */
Justin Klaassen44595162015-05-28 17:55:20 -0700487 public boolean isScrollable() {
488 return mScrollable;
Hans Boehm61568a12015-05-18 18:25:41 -0700489 }
490
Hans Boehm013969e2015-04-13 20:29:47 -0700491 int getCurrentCharPos() {
492 synchronized(mWidthLock) {
Hans Boehma0e45f32015-05-30 13:20:35 -0700493 return (int) Math.round(mCurrentPos / mCharWidth);
Hans Boehm013969e2015-04-13 20:29:47 -0700494 }
495 }
496
Hans Boehm84614952014-11-25 18:46:17 -0800497 void clear() {
Hans Boehm760a9dc2015-04-20 10:27:12 -0700498 mValid = false;
Hans Boehm1176f232015-05-11 16:26:03 -0700499 mScrollable = false;
Hans Boehm84614952014-11-25 18:46:17 -0800500 setText("");
501 }
502
503 void redisplay() {
Hans Boehm013969e2015-04-13 20:29:47 -0700504 int currentCharPos = getCurrentCharPos();
Hans Boehm84614952014-11-25 18:46:17 -0800505 int maxChars = getMaxChars();
Hans Boehmf6dae112015-06-18 17:57:50 -0700506 int lastDisplayedDigit[] = new int[1];
507 String result = getFormattedResult(currentCharPos, maxChars, lastDisplayedDigit, false);
Hans Boehm84614952014-11-25 18:46:17 -0800508 int epos = result.indexOf('e');
Hans Boehm013969e2015-04-13 20:29:47 -0700509 result = KeyMaps.translateResult(result);
Hans Boehm84614952014-11-25 18:46:17 -0800510 if (epos > 0 && result.indexOf('.') == -1) {
511 // Gray out exponent if used as position indicator
512 SpannableString formattedResult = new SpannableString(result);
Hans Boehm1176f232015-05-11 16:26:03 -0700513 formattedResult.setSpan(mExponentColorSpan, epos, result.length(),
Hans Boehm84614952014-11-25 18:46:17 -0800514 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
515 setText(formattedResult);
516 } else {
517 setText(result);
518 }
Hans Boehmf6dae112015-06-18 17:57:50 -0700519 mLastDisplayedDigit = lastDisplayedDigit[0];
Hans Boehm760a9dc2015-04-20 10:27:12 -0700520 mValid = true;
Hans Boehm84614952014-11-25 18:46:17 -0800521 }
522
523 @Override
524 public void computeScroll() {
525 if (!mScrollable) return;
526 if (mScroller.computeScrollOffset()) {
527 mCurrentPos = mScroller.getCurrX();
528 if (mCurrentPos != mLastPos) {
529 mLastPos = mCurrentPos;
530 redisplay();
531 }
532 if (!mScroller.isFinished()) {
Justin Klaassen44595162015-05-28 17:55:20 -0700533 postInvalidateOnAnimation();
Hans Boehm84614952014-11-25 18:46:17 -0800534 }
535 }
536 }
537
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700538 // Copy support:
539
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700540 private ActionMode.Callback mCopyActionModeCallback = new ActionMode.Callback() {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700541 @Override
542 public boolean onCreateActionMode(ActionMode mode, Menu menu) {
543 MenuInflater inflater = mode.getMenuInflater();
544 inflater.inflate(R.menu.copy, menu);
545 return true;
546 }
547
548 @Override
549 public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
550 return false; // Return false if nothing is done
551 }
552
553 @Override
554 public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
555 switch (item.getItemId()) {
556 case R.id.menu_copy:
557 copyContent();
558 mode.finish();
559 return true;
560 default:
561 return false;
562 }
563 }
564
565 @Override
566 public void onDestroyActionMode(ActionMode mode) {
Hans Boehm1176f232015-05-11 16:26:03 -0700567 mActionMode = null;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700568 }
569 };
570
Hans Boehm1176f232015-05-11 16:26:03 -0700571 public boolean stopActionMode() {
572 if (mActionMode != null) {
573 mActionMode.finish();
574 return true;
575 }
576 return false;
577 }
578
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700579 private void setPrimaryClip(ClipData clip) {
580 ClipboardManager clipboard = (ClipboardManager) getContext().
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700581 getSystemService(Context.CLIPBOARD_SERVICE);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700582 clipboard.setPrimaryClip(clip);
583 }
584
585 private void copyContent() {
586 final CharSequence text = getFullText();
587 ClipboardManager clipboard =
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700588 (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
589 // We include a tag URI, to allow us to recognize our own results and handle them
590 // specially.
591 ClipData.Item newItem = new ClipData.Item(text, null, mEvaluator.capture());
592 String[] mimeTypes = new String[] {ClipDescription.MIMETYPE_TEXT_PLAIN};
593 ClipData cd = new ClipData("calculator result", mimeTypes, newItem);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700594 clipboard.setPrimaryClip(cd);
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700595 Toast.makeText(getContext(), R.string.text_copied_toast, Toast.LENGTH_SHORT).show();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700596 }
597
Hans Boehm84614952014-11-25 18:46:17 -0800598}