blob: 0c4c18fd892cc8637196384b906f5953804b41c7 [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;
Hans Boehm7f83e362015-06-10 15:41:04 -070023import android.graphics.Rect;
Justin Klaassen44595162015-05-28 17:55:20 -070024import android.text.Layout;
Hans Boehm7f83e362015-06-10 15:41:04 -070025import android.text.Spannable;
Hans Boehm84614952014-11-25 18:46:17 -080026import android.text.SpannableString;
Hans Boehm1176f232015-05-11 16:26:03 -070027import android.text.Spanned;
Justin Klaassen44595162015-05-28 17:55:20 -070028import android.text.TextPaint;
Hans Boehm7f83e362015-06-10 15:41:04 -070029import android.text.style.BackgroundColorSpan;
Hans Boehm84614952014-11-25 18:46:17 -080030import android.text.style.ForegroundColorSpan;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070031import android.util.AttributeSet;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070032import android.view.ActionMode;
33import android.view.GestureDetector;
34import android.view.Menu;
35import android.view.MenuInflater;
36import android.view.MenuItem;
37import android.view.MotionEvent;
38import android.view.View;
Justin Klaassen44595162015-05-28 17:55:20 -070039import android.widget.OverScroller;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070040import android.widget.Toast;
Hans Boehm84614952014-11-25 18:46:17 -080041
Hans Boehm84614952014-11-25 18:46:17 -080042// A text widget that is "infinitely" scrollable to the right,
43// and obtains the text to display via a callback to Logic.
Justin Klaassen44595162015-05-28 17:55:20 -070044public class CalculatorResult extends AlignedTextView {
Hans Boehm61568a12015-05-18 18:25:41 -070045 static final int MAX_RIGHT_SCROLL = 10000000;
Hans Boehm08e8f322015-04-21 13:18:38 -070046 static final int INVALID = MAX_RIGHT_SCROLL + 10000;
Hans Boehm84614952014-11-25 18:46:17 -080047 // A larger value is unlikely to avoid running out of space
48 final OverScroller mScroller;
49 final GestureDetector mGestureDetector;
50 class MyTouchListener implements View.OnTouchListener {
51 @Override
52 public boolean onTouch(View v, MotionEvent event) {
Justin Klaassen44595162015-05-28 17:55:20 -070053 return mGestureDetector.onTouchEvent(event);
Hans Boehm84614952014-11-25 18:46:17 -080054 }
55 }
56 final MyTouchListener mTouchListener = new MyTouchListener();
57 private Evaluator mEvaluator;
58 private boolean mScrollable = false;
59 // A scrollable result is currently displayed.
Hans Boehm760a9dc2015-04-20 10:27:12 -070060 private boolean mValid = false;
Hans Boehmc01cd7f2015-05-12 18:32:19 -070061 // The result holds something valid; either a a number or an error
62 // message.
63 private int mCurrentPos;// Position of right of display relative to decimal point, in pixels.
64 // Large positive values mean the decimal point is scrolled off the
65 // left of the display. Zero means decimal point is barely displayed
66 // on the right.
Hans Boehm61568a12015-05-18 18:25:41 -070067 private int mLastPos; // Position already reflected in display. Pixels.
68 private int mMinPos; // Minimum position before all digits disappear off the right. Pixels.
69 private int mMaxPos; // Maximum position before we start displaying the infinite
70 // sequence of trailing zeroes on the right. Pixels.
Hans Boehma0e45f32015-05-30 13:20:35 -070071 private int mMaxCharPos; // The same, but in characters.
72 private int mLsd; // Position of least-significant digit in result
73 // (1 = tenths, -1 = tens), or Integer.MAX_VALUE.
Justin Klaassen44595162015-05-28 17:55:20 -070074 private final Object mWidthLock = new Object();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070075 // Protects the next two fields.
76 private int mWidthConstraint = -1;
Hans Boehma0e45f32015-05-30 13:20:35 -070077 // Our total width in pixels minus space for ellipsis.
Justin Klaassen44595162015-05-28 17:55:20 -070078 private float mCharWidth = 1;
Hans Boehmc01cd7f2015-05-12 18:32:19 -070079 // Maximum character width. For now we pretend that all characters
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070080 // have this width.
Hans Boehmc01cd7f2015-05-12 18:32:19 -070081 // TODO: We're not really using a fixed width font. But it appears
82 // to be close enough for the characters we use that the difference
83 // is not noticeable.
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070084 private static final int MAX_WIDTH = 100;
85 // Maximum number of digits displayed
Hans Boehm50ed3202015-06-09 14:35:49 -070086 public static final int MAX_LEADING_ZEROES = 6;
Hans Boehma0e45f32015-05-30 13:20:35 -070087 // Maximum number of leading zeroes after decimal point before we
88 // switch to scientific notation with negative exponent.
Hans Boehm50ed3202015-06-09 14:35:49 -070089 public static final int MAX_TRAILING_ZEROES = 6;
Hans Boehma0e45f32015-05-30 13:20:35 -070090 // Maximum number of trailing zeroes before the decimal point before
91 // we switch to scientific notation with positive exponent.
92 private static final int SCI_NOTATION_EXTRA = 1;
93 // Extra digits for standard scientific notation. In this case we
94 // have a deecimal point and no ellipsis.
Hans Boehm1176f232015-05-11 16:26:03 -070095 private ActionMode mActionMode;
96 private final ForegroundColorSpan mExponentColorSpan;
Hans Boehm84614952014-11-25 18:46:17 -080097
98 public CalculatorResult(Context context, AttributeSet attrs) {
99 super(context, attrs);
100 mScroller = new OverScroller(context);
101 mGestureDetector = new GestureDetector(context,
102 new GestureDetector.SimpleOnGestureListener() {
103 @Override
Justin Klaassend48b7562015-04-16 16:51:38 -0700104 public boolean onDown(MotionEvent e) {
105 return true;
106 }
107 @Override
Hans Boehm84614952014-11-25 18:46:17 -0800108 public boolean onFling(MotionEvent e1, MotionEvent e2,
109 float velocityX, float velocityY) {
110 if (!mScroller.isFinished()) {
111 mCurrentPos = mScroller.getFinalX();
112 }
113 mScroller.forceFinished(true);
Hans Boehm1176f232015-05-11 16:26:03 -0700114 stopActionMode();
Hans Boehmfbcef702015-04-27 18:07:47 -0700115 CalculatorResult.this.cancelLongPress();
116 // Ignore scrolls of error string, etc.
117 if (!mScrollable) return true;
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700118 mScroller.fling(mCurrentPos, 0, - (int) velocityX, 0 /* horizontal only */,
Hans Boehm61568a12015-05-18 18:25:41 -0700119 mMinPos, mMaxPos, 0, 0);
Justin Klaassen44595162015-05-28 17:55:20 -0700120 postInvalidateOnAnimation();
Hans Boehm84614952014-11-25 18:46:17 -0800121 return true;
122 }
123 @Override
124 public boolean onScroll(MotionEvent e1, MotionEvent e2,
125 float distanceX, float distanceY) {
Hans Boehm61568a12015-05-18 18:25:41 -0700126 int distance = (int)distanceX;
Hans Boehm84614952014-11-25 18:46:17 -0800127 if (!mScroller.isFinished()) {
128 mCurrentPos = mScroller.getFinalX();
129 }
130 mScroller.forceFinished(true);
Hans Boehm1176f232015-05-11 16:26:03 -0700131 stopActionMode();
Hans Boehm84614952014-11-25 18:46:17 -0800132 CalculatorResult.this.cancelLongPress();
133 if (!mScrollable) return true;
Hans Boehm61568a12015-05-18 18:25:41 -0700134 if (mCurrentPos + distance < mMinPos) {
135 distance = mMinPos - mCurrentPos;
136 } else if (mCurrentPos + distance > mMaxPos) {
137 distance = mMaxPos - mCurrentPos;
138 }
Hans Boehm84614952014-11-25 18:46:17 -0800139 int duration = (int)(e2.getEventTime() - e1.getEventTime());
140 if (duration < 1 || duration > 100) duration = 10;
Hans Boehm61568a12015-05-18 18:25:41 -0700141 mScroller.startScroll(mCurrentPos, 0, distance, 0, (int)duration);
Justin Klaassen44595162015-05-28 17:55:20 -0700142 postInvalidateOnAnimation();
Hans Boehm84614952014-11-25 18:46:17 -0800143 return true;
144 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700145 @Override
146 public void onLongPress(MotionEvent e) {
Hans Boehm1176f232015-05-11 16:26:03 -0700147 if (mValid) {
148 mActionMode = startActionMode(mCopyActionModeCallback,
149 ActionMode.TYPE_FLOATING);
150 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700151 }
Hans Boehm84614952014-11-25 18:46:17 -0800152 });
153 setOnTouchListener(mTouchListener);
154 setHorizontallyScrolling(false); // do it ourselves
155 setCursorVisible(false);
Hans Boehm1176f232015-05-11 16:26:03 -0700156 mExponentColorSpan = new ForegroundColorSpan(
157 context.getColor(R.color.display_result_exponent_text_color));
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700158
159 // Copy ActionMode is triggered explicitly, not through
160 // setCustomSelectionActionModeCallback.
Hans Boehm84614952014-11-25 18:46:17 -0800161 }
162
163 void setEvaluator(Evaluator evaluator) {
164 mEvaluator = evaluator;
165 }
166
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700167 @Override
168 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
169 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
170
Justin Klaassen44595162015-05-28 17:55:20 -0700171 final TextPaint paint = getPaint();
172 final int newWidthConstraint = MeasureSpec.getSize(widthMeasureSpec)
173 - (getPaddingLeft() + getPaddingRight())
174 - (int) Math.ceil(Layout.getDesiredWidth(KeyMaps.ELLIPSIS, paint));
175 final float newCharWidth = Layout.getDesiredWidth("\u2007", paint);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700176 synchronized(mWidthLock) {
Hans Boehm013969e2015-04-13 20:29:47 -0700177 mWidthConstraint = newWidthConstraint;
178 mCharWidth = newCharWidth;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700179 }
180 }
181
Hans Boehma0e45f32015-05-30 13:20:35 -0700182 // Return the length of the exponent representation for the given exponent, in
183 // characters.
184 private final int expLen(int exp) {
185 if (exp == 0) return 0;
186 return (int)Math.ceil(Math.log10(Math.abs((double)exp))) + (exp >= 0 ? 1 : 2);
Hans Boehm61568a12015-05-18 18:25:41 -0700187 }
188
Hans Boehma0e45f32015-05-30 13:20:35 -0700189 /**
190 * Initiate display of a new result.
191 * The parameters specify various properties of the result.
192 * @param initPrec Initial display precision computed by evaluator. (1 = tenths digit)
193 * @param msd Position of most significant digit. Offset from left of string.
194 Evaluator.INVALID_MSD if unknown.
195 * @param leastDigPos Position of least significant digit (1 = tenths digit)
196 * or Integer.MAX_VALUE.
197 * @param truncatedWholePart Result up to but not including decimal point.
198 Currently we only use the length.
199 */
200 void displayResult(int initPrec, int msd, int leastDigPos, String truncatedWholePart) {
201 initPositions(initPrec, msd, leastDigPos, truncatedWholePart);
Hans Boehm84614952014-11-25 18:46:17 -0800202 redisplay();
203 }
204
Hans Boehma0e45f32015-05-30 13:20:35 -0700205 /**
206 * Set up scroll bounds and determine whether the result is scrollable, based on the
207 * supplied information about the result.
208 * This is unfortunately complicated because we need to predict whether trailing digits
209 * will eventually be replaced by an exponent.
210 * Just appending the exponent during formatting would be simpler, but would produce
211 * jumpier results during transitions.
212 */
213 private void initPositions(int initPrec, int msd, int leastDigPos, String truncatedWholePart) {
214 float charWidth;
215 int maxChars = getMaxChars();
216 mLastPos = INVALID;
217 mLsd = leastDigPos;
218 synchronized(mWidthLock) {
219 charWidth = mCharWidth;
220 }
221 mCurrentPos = mMinPos = (int) Math.round(initPrec * charWidth);
222 // Prevent scrolling past initial position, which is calculated to show leading digits.
223 if (msd == Evaluator.INVALID_MSD) {
224 // Possible zero value
225 if (leastDigPos == Integer.MIN_VALUE) {
226 // Definite zero value.
227 mMaxPos = mMinPos;
228 mMaxCharPos = (int) Math.round(mMaxPos/charWidth);
229 mScrollable = false;
230 } else {
231 // May be very small nonzero value. Allow user to find out.
232 mMaxPos = mMaxCharPos = MAX_RIGHT_SCROLL;
233 mScrollable = true;
234 }
235 return;
236 }
237 int wholeLen = truncatedWholePart.length();
238 int negative = truncatedWholePart.charAt(0) == '-' ? 1 : 0;
239 boolean adjustedForExp = false; // Adjusted for normal exponent.
240 if (msd > wholeLen && msd <= wholeLen + 3) {
241 // Avoid tiny negative exponent; pretend msd is just to the right of decimal point.
242 msd = wholeLen - 1;
243 }
244 int minCharPos = msd - negative - wholeLen;
245 // Position of leftmost significant digit relative to dec. point.
246 // Usually negative.
247 mMaxCharPos = MAX_RIGHT_SCROLL; // How far does it make sense to scroll right?
248 // If msd is left of decimal point should logically be
249 // mMinPos = - (int) Math.ceil(getPaint().measureText(truncatedWholePart)), but
250 // we eventually translate to a character position by dividing by mCharWidth.
251 // To avoid rounding issues, we use the analogous computation here.
252 if (minCharPos > -1 && minCharPos < MAX_LEADING_ZEROES + 2) {
253 // Small number of leading zeroes, avoid scientific notation.
254 minCharPos = -1;
255 }
256 if (leastDigPos < MAX_RIGHT_SCROLL) {
257 mMaxCharPos = leastDigPos;
258 if (mMaxCharPos < -1 && mMaxCharPos > -(MAX_TRAILING_ZEROES + 2)) {
259 mMaxCharPos = -1;
260 }
261 // leastDigPos is positive or negative, never 0.
262 if (mMaxCharPos < -1) {
263 // Number entirely to left of decimal point.
264 // We'll need a positive exponent or displayed zeros to display entire number.
265 mMaxCharPos = Math.min(-1, mMaxCharPos + expLen(-minCharPos - 1));
266 if (mMaxCharPos >= -1) {
267 // Unlikely; huge exponent.
268 mMaxCharPos = -1;
269 } else {
270 adjustedForExp = true;
271 }
272 } else if (minCharPos > -1 || mMaxCharPos >= maxChars) {
273 // Number either entirely to the right of decimal point, or decimal point not
274 // visible when scrolled to the right.
275 // We will need an exponent when looking at the rightmost digit.
276 // Allow additional scrolling to make room.
277 mMaxCharPos += expLen(-(minCharPos + 1));
278 adjustedForExp = true;
279 // Assumed an exponent for standard scientific notation for now.
280 // Adjusted below if necessary.
281 }
282 mScrollable = (mMaxCharPos - minCharPos + negative >= maxChars);
283 if (mScrollable) {
284 if (adjustedForExp) {
285 // We may need a slightly larger negative exponent while scrolling.
286 mMaxCharPos += expLen(-leastDigPos) - expLen(-(minCharPos + 1));
287 }
288 }
289 mMaxPos = Math.min((int) Math.round(mMaxCharPos * charWidth), MAX_RIGHT_SCROLL);
290 if (!mScrollable) {
291 // Position the number consistently with our assumptions to make sure it
292 // actually fits.
293 mCurrentPos = mMaxPos;
294 }
295 } else {
296 mMaxPos = mMaxCharPos = MAX_RIGHT_SCROLL;
297 mScrollable = true;
298 }
299 }
300
Hans Boehm84614952014-11-25 18:46:17 -0800301 void displayError(int resourceId) {
Hans Boehm760a9dc2015-04-20 10:27:12 -0700302 mValid = true;
Hans Boehm84614952014-11-25 18:46:17 -0800303 mScrollable = false;
304 setText(resourceId);
305 }
306
Hans Boehm013969e2015-04-13 20:29:47 -0700307 private final int MAX_COPY_SIZE = 1000000;
308
Hans Boehma0e45f32015-05-30 13:20:35 -0700309 /*
310 * Return the most significant digit position in the given string or Evaluator.INVALID_MSD.
311 * Unlike Evaluator.getMsdPos, we treat a final 1 as significant.
312 */
313 public static int getNaiveMsdPos(String s) {
314 int len = s.length();
315 int nonzeroPos = -1;
316 for (int i = 0; i < len; ++i) {
317 char c = s.charAt(i);
318 if (c != '-' && c != '.' && c != '0') {
319 return i;
320 }
321 }
322 return Evaluator.INVALID_MSD;
323 }
324
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700325 // Format a result returned by Evaluator.getString() into a single line containing ellipses
Hans Boehma0e45f32015-05-30 13:20:35 -0700326 // (if appropriate) and an exponent (if appropriate). prec is the value that was passed to
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700327 // getString and thus identifies the significance of the rightmost digit.
Hans Boehma0e45f32015-05-30 13:20:35 -0700328 // A value of 1 means the rightmost digits corresponds to tenths.
329 // maxDigs is the maximum number of characters in the result.
Hans Boehm08e8f322015-04-21 13:18:38 -0700330 // We add two distinct kinds of exponents:
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700331 // 1) If the final result contains the leading digit we use standard scientific notation.
332 // 2) If not, we add an exponent corresponding to an interpretation of the final result as
333 // an integer.
Hans Boehm08e8f322015-04-21 13:18:38 -0700334 // We add an ellipsis on the left if the result was truncated.
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700335 // We add ellipses and exponents in a way that leaves most digits in the position they
336 // would have been in had we not done so.
337 // This minimizes jumps as a result of scrolling. Result is NOT internationalized,
338 // uses "e" for exponent.
Hans Boehma0e45f32015-05-30 13:20:35 -0700339 public String formatResult(String res, int prec,
Hans Boehm08e8f322015-04-21 13:18:38 -0700340 int maxDigs, boolean truncated,
341 boolean negative) {
Hans Boehma0e45f32015-05-30 13:20:35 -0700342 int msd; // Position of most significant digit in res or indication its outside res.
343 int minusSpace = negative ? 1 : 0;
Hans Boehm08e8f322015-04-21 13:18:38 -0700344 if (truncated) {
345 res = KeyMaps.ELLIPSIS + res.substring(1, res.length());
Hans Boehma0e45f32015-05-30 13:20:35 -0700346 msd = -1;
347 } else {
348 msd = getNaiveMsdPos(res); // INVALID_MSD is OK and is treated as large.
Hans Boehm08e8f322015-04-21 13:18:38 -0700349 }
350 int decIndex = res.indexOf('.');
351 int resLen = res.length();
Hans Boehma0e45f32015-05-30 13:20:35 -0700352 if ((decIndex == -1 || msd != Evaluator.INVALID_MSD
353 && msd - decIndex > MAX_LEADING_ZEROES + 1) && prec != -1) {
354 // No decimal point displayed, and it's not just to the right of the last digit,
355 // or we should suppress leading zeroes.
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700356 // Add an exponent to let the user track which digits are currently displayed.
357 // This is a bit tricky, since the number of displayed digits affects the displayed
358 // exponent, which can affect the room we have for mantissa digits. We occasionally
359 // display one digit too few. This is sometimes unavoidable, but we could
Hans Boehm08e8f322015-04-21 13:18:38 -0700360 // avoid it in more cases.
Hans Boehma0e45f32015-05-30 13:20:35 -0700361 int exp = prec > 0 ? -prec : -prec - 1;
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700362 // Can be used as TYPE (2) EXPONENT. -1 accounts for decimal point.
Hans Boehm08e8f322015-04-21 13:18:38 -0700363 boolean hasPoint = false;
Hans Boehma0e45f32015-05-30 13:20:35 -0700364 if (msd < maxDigs - 1 && msd >= 0 &&
365 resLen - msd + 1 /* dec. pt. */ + minusSpace <= maxDigs + SCI_NOTATION_EXTRA) {
Hans Boehm08e8f322015-04-21 13:18:38 -0700366 // TYPE (1) EXPONENT computation and transformation:
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700367 // Leading digit is in display window. Use standard calculator scientific notation
368 // with one digit to the left of the decimal point. Insert decimal point and
369 // delete leading zeroes.
Hans Boehma0e45f32015-05-30 13:20:35 -0700370 // We try to keep leading digits roughly in position, and never
371 // lengthen the result by more than SCI_NOT_EXTRA.
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700372 String fraction = res.substring(msd + 1, resLen);
Hans Boehma0e45f32015-05-30 13:20:35 -0700373 res = (negative ? "-" : "") + res.substring(msd, msd + 1) + "." + fraction;
Hans Boehm08e8f322015-04-21 13:18:38 -0700374 exp += resLen - msd - 1;
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700375 // Original exp was correct for decimal point at right of fraction.
376 // Adjust by length of fraction.
Hans Boehm08e8f322015-04-21 13:18:38 -0700377 resLen = res.length();
378 hasPoint = true;
379 }
380 if (exp != 0 || truncated) {
381 // Actually add the exponent of either type:
382 String expAsString = Integer.toString(exp);
383 int expDigits = expAsString.length();
384 int dropDigits = expDigits + 1;
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700385 // Drop digits even if there is room. Otherwise the scrolling gets jumpy.
Hans Boehm08e8f322015-04-21 13:18:38 -0700386 if (dropDigits >= resLen - 1) {
387 dropDigits = Math.max(resLen - 2, 0);
Hans Boehma0e45f32015-05-30 13:20:35 -0700388 // Jumpy is better than no mantissa. Probably impossible anyway.
Hans Boehm08e8f322015-04-21 13:18:38 -0700389 }
390 if (!hasPoint) {
391 // Special handling for TYPE(2) EXPONENT:
392 exp += dropDigits;
393 expAsString = Integer.toString(exp);
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700394 // Adjust for digits we are about to drop to drop to make room for exponent.
395 // This can affect the room we have for the mantissa. We adjust only for
396 // positive exponents, when it could otherwise result in a truncated
Hans Boehm08e8f322015-04-21 13:18:38 -0700397 // displayed result.
398 if (exp > 0 && expAsString.length() > expDigits) {
399 // ++expDigits; (dead code)
400 ++dropDigits;
401 ++exp;
Hans Boehma0e45f32015-05-30 13:20:35 -0700402 expAsString = Integer.toString(exp);
Hans Boehm08e8f322015-04-21 13:18:38 -0700403 // This cannot increase the length a second time.
404 }
Hans Boehma0e45f32015-05-30 13:20:35 -0700405 if (prec - dropDigits > mLsd) {
406 // This can happen if e.g. result = 10^40 + 10^10
407 // It turns out we would otherwise display ...10e9 because
408 // it takes the same amount of space as ...1e10 but shows one more digit.
409 // But we don't want to display a trailing zero, even if it's free.
410 ++dropDigits;
411 ++exp;
412 expAsString = Integer.toString(exp);
413 }
Hans Boehm08e8f322015-04-21 13:18:38 -0700414 }
415 res = res.substring(0, resLen - dropDigits);
416 res = res + "e" + expAsString;
417 } // else don't add zero exponent
418 }
419 return res;
420 }
421
422 // Get formatted, but not internationalized, result from
423 // mEvaluator.
424 private String getFormattedResult(int pos, int maxSize) {
425 final boolean truncated[] = new boolean[1];
426 final boolean negative[] = new boolean[1];
427 final int requested_prec[] = {pos};
Hans Boehma0e45f32015-05-30 13:20:35 -0700428 final String raw_res = mEvaluator.getString(requested_prec, mMaxCharPos,
429 maxSize, truncated, negative);
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700430 return formatResult(raw_res, requested_prec[0], maxSize, truncated[0], negative[0]);
Hans Boehm08e8f322015-04-21 13:18:38 -0700431 }
432
Hans Boehm84614952014-11-25 18:46:17 -0800433 // Return entire result (within reason) up to current displayed precision.
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700434 public String getFullText() {
Hans Boehm760a9dc2015-04-20 10:27:12 -0700435 if (!mValid) return "";
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700436 if (!mScrollable) return getText().toString();
Hans Boehm013969e2015-04-13 20:29:47 -0700437 int currentCharPos = getCurrentCharPos();
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700438 return KeyMaps.translateResult(getFormattedResult(currentCharPos, MAX_COPY_SIZE));
Hans Boehm84614952014-11-25 18:46:17 -0800439 }
440
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700441 public boolean fullTextIsExact() {
442 BoundedRational rat = mEvaluator.getRational();
Hans Boehm013969e2015-04-13 20:29:47 -0700443 int currentCharPos = getCurrentCharPos();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700444 if (currentCharPos == -1) {
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700445 // Suppressing decimal point; still showing all integral digits.
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700446 currentCharPos = 0;
447 }
448 // TODO: Could handle scientific notation cases better;
449 // We currently treat those conservatively as approximate.
450 return (currentCharPos >= BoundedRational.digitsRequired(rat));
451 }
452
Hans Boehm61568a12015-05-18 18:25:41 -0700453 /**
454 * Return the maximum number of characters that will fit in the result display.
455 * May be called asynchronously from non-UI thread.
456 */
Hans Boehm84614952014-11-25 18:46:17 -0800457 int getMaxChars() {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700458 int result;
459 synchronized(mWidthLock) {
Justin Klaassen44595162015-05-28 17:55:20 -0700460 result = (int) Math.floor(mWidthConstraint / mCharWidth);
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700461 // We can apparently finish evaluating before onMeasure in CalculatorText has been
462 // called, in which case we get 0 or -1 as the width constraint.
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700463 }
Hans Boehm84614952014-11-25 18:46:17 -0800464 if (result <= 0) {
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700465 // Return something conservatively big, to force sufficient evaluation.
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700466 return MAX_WIDTH;
Hans Boehm84614952014-11-25 18:46:17 -0800467 } else {
Justin Klaassen44595162015-05-28 17:55:20 -0700468 // Always allow for the ellipsis character which already accounted for in the width
469 // constraint.
470 return result + 1;
Hans Boehm84614952014-11-25 18:46:17 -0800471 }
472 }
473
Hans Boehm61568a12015-05-18 18:25:41 -0700474 /**
Justin Klaassen44595162015-05-28 17:55:20 -0700475 * @return {@code true} if the currently displayed result is scrollable
Hans Boehm61568a12015-05-18 18:25:41 -0700476 */
Justin Klaassen44595162015-05-28 17:55:20 -0700477 public boolean isScrollable() {
478 return mScrollable;
Hans Boehm61568a12015-05-18 18:25:41 -0700479 }
480
Hans Boehm013969e2015-04-13 20:29:47 -0700481 int getCurrentCharPos() {
482 synchronized(mWidthLock) {
Hans Boehma0e45f32015-05-30 13:20:35 -0700483 return (int) Math.round(mCurrentPos / mCharWidth);
Hans Boehm013969e2015-04-13 20:29:47 -0700484 }
485 }
486
Hans Boehm84614952014-11-25 18:46:17 -0800487 void clear() {
Hans Boehm760a9dc2015-04-20 10:27:12 -0700488 mValid = false;
Hans Boehm1176f232015-05-11 16:26:03 -0700489 mScrollable = false;
Hans Boehm84614952014-11-25 18:46:17 -0800490 setText("");
491 }
492
493 void redisplay() {
Hans Boehm013969e2015-04-13 20:29:47 -0700494 int currentCharPos = getCurrentCharPos();
Hans Boehm84614952014-11-25 18:46:17 -0800495 int maxChars = getMaxChars();
Hans Boehm08e8f322015-04-21 13:18:38 -0700496 String result = getFormattedResult(currentCharPos, maxChars);
Hans Boehm84614952014-11-25 18:46:17 -0800497 int epos = result.indexOf('e');
Hans Boehm013969e2015-04-13 20:29:47 -0700498 result = KeyMaps.translateResult(result);
Hans Boehm84614952014-11-25 18:46:17 -0800499 if (epos > 0 && result.indexOf('.') == -1) {
500 // Gray out exponent if used as position indicator
501 SpannableString formattedResult = new SpannableString(result);
Hans Boehm1176f232015-05-11 16:26:03 -0700502 formattedResult.setSpan(mExponentColorSpan, epos, result.length(),
Hans Boehm84614952014-11-25 18:46:17 -0800503 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
504 setText(formattedResult);
505 } else {
506 setText(result);
507 }
Hans Boehm760a9dc2015-04-20 10:27:12 -0700508 mValid = true;
Hans Boehm84614952014-11-25 18:46:17 -0800509 }
510
511 @Override
512 public void computeScroll() {
513 if (!mScrollable) return;
514 if (mScroller.computeScrollOffset()) {
515 mCurrentPos = mScroller.getCurrX();
516 if (mCurrentPos != mLastPos) {
517 mLastPos = mCurrentPos;
518 redisplay();
519 }
520 if (!mScroller.isFinished()) {
Justin Klaassen44595162015-05-28 17:55:20 -0700521 postInvalidateOnAnimation();
Hans Boehm84614952014-11-25 18:46:17 -0800522 }
523 }
524 }
525
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700526 // Copy support:
527
Hans Boehm7f83e362015-06-10 15:41:04 -0700528 private ActionMode.Callback2 mCopyActionModeCallback = new ActionMode.Callback2() {
529
530 private BackgroundColorSpan mHighlightSpan;
531
532 private void highlightResult() {
533 final Spannable text = (Spannable) getText();
534 mHighlightSpan = new BackgroundColorSpan(getHighlightColor());
535 text.setSpan(mHighlightSpan, 0, text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
536 }
537
538 private void unhighlightResult() {
539 final Spannable text = (Spannable) getText();
540 text.removeSpan(mHighlightSpan);
541 }
542
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700543 @Override
544 public boolean onCreateActionMode(ActionMode mode, Menu menu) {
545 MenuInflater inflater = mode.getMenuInflater();
546 inflater.inflate(R.menu.copy, menu);
Hans Boehm7f83e362015-06-10 15:41:04 -0700547 highlightResult();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700548 return true;
549 }
550
551 @Override
552 public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
553 return false; // Return false if nothing is done
554 }
555
556 @Override
557 public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
558 switch (item.getItemId()) {
559 case R.id.menu_copy:
560 copyContent();
561 mode.finish();
562 return true;
563 default:
564 return false;
565 }
566 }
567
568 @Override
569 public void onDestroyActionMode(ActionMode mode) {
Hans Boehm7f83e362015-06-10 15:41:04 -0700570 unhighlightResult();
Hans Boehm1176f232015-05-11 16:26:03 -0700571 mActionMode = null;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700572 }
Hans Boehm7f83e362015-06-10 15:41:04 -0700573
574 @Override
575 public void onGetContentRect(ActionMode mode, View view, Rect outRect) {
576 super.onGetContentRect(mode, view, outRect);
577 outRect.left += getPaddingLeft();
578 outRect.top += getPaddingTop();
579 outRect.right -= getPaddingRight();
580 outRect.bottom -= getPaddingBottom();
581 final int width = (int) Layout.getDesiredWidth(getText(), getPaint());
582 if (width < outRect.width()) {
583 outRect.left = outRect.right - width;
584 }
585 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700586 };
587
Hans Boehm1176f232015-05-11 16:26:03 -0700588 public boolean stopActionMode() {
589 if (mActionMode != null) {
590 mActionMode.finish();
591 return true;
592 }
593 return false;
594 }
595
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700596 private void setPrimaryClip(ClipData clip) {
597 ClipboardManager clipboard = (ClipboardManager) getContext().
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700598 getSystemService(Context.CLIPBOARD_SERVICE);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700599 clipboard.setPrimaryClip(clip);
600 }
601
602 private void copyContent() {
603 final CharSequence text = getFullText();
604 ClipboardManager clipboard =
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700605 (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
606 // We include a tag URI, to allow us to recognize our own results and handle them
607 // specially.
608 ClipData.Item newItem = new ClipData.Item(text, null, mEvaluator.capture());
609 String[] mimeTypes = new String[] {ClipDescription.MIMETYPE_TEXT_PLAIN};
610 ClipData cd = new ClipData("calculator result", mimeTypes, newItem);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700611 clipboard.setPrimaryClip(cd);
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700612 Toast.makeText(getContext(), R.string.text_copied_toast, Toast.LENGTH_SHORT).show();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700613 }
614
Hans Boehm84614952014-11-25 18:46:17 -0800615}