blob: 289e13d2bf14c120d9555010fcbf5ff3dfef0777 [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.
Hans Boehm5e802f32015-06-22 17:18:52 -070063 // A suffix of "Pos" denotes a pixel offset. Zero represents a scroll position
64 // in which the decimal point is just barely visible on the right of the display.
Hans Boehmc01cd7f2015-05-12 18:32:19 -070065 private int mCurrentPos;// Position of right of display relative to decimal point, in pixels.
66 // Large positive values mean the decimal point is scrolled off the
67 // left of the display. Zero means decimal point is barely displayed
68 // on the right.
Hans Boehm61568a12015-05-18 18:25:41 -070069 private int mLastPos; // Position already reflected in display. Pixels.
70 private int mMinPos; // Minimum position before all digits disappear off the right. Pixels.
71 private int mMaxPos; // Maximum position before we start displaying the infinite
72 // sequence of trailing zeroes on the right. Pixels.
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.
79 // TODO: Apply the same convention to other classes.
80 private int mMaxCharOffset; // Character offset from decimal point of rightmost digit
81 // that should be displayed. Essentially the same as
82 private int mLsdOffset; // Position of least-significant digit in result
83 private int mLastDisplayedOffset; // Offset of last digit actually displayed after adding
Hans Boehmf6dae112015-06-18 17:57:50 -070084 // exponent.
Justin Klaassen44595162015-05-28 17:55:20 -070085 private final Object mWidthLock = new Object();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070086 // Protects the next two fields.
87 private int mWidthConstraint = -1;
Hans Boehma0e45f32015-05-30 13:20:35 -070088 // Our total width in pixels minus space for ellipsis.
Justin Klaassen44595162015-05-28 17:55:20 -070089 private float mCharWidth = 1;
Hans Boehmc01cd7f2015-05-12 18:32:19 -070090 // Maximum character width. For now we pretend that all characters
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070091 // have this width.
Hans Boehmc01cd7f2015-05-12 18:32:19 -070092 // TODO: We're not really using a fixed width font. But it appears
93 // to be close enough for the characters we use that the difference
94 // is not noticeable.
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070095 private static final int MAX_WIDTH = 100;
96 // Maximum number of digits displayed
Hans Boehm50ed3202015-06-09 14:35:49 -070097 public static final int MAX_LEADING_ZEROES = 6;
Hans Boehma0e45f32015-05-30 13:20:35 -070098 // Maximum number of leading zeroes after decimal point before we
99 // switch to scientific notation with negative exponent.
Hans Boehm50ed3202015-06-09 14:35:49 -0700100 public static final int MAX_TRAILING_ZEROES = 6;
Hans Boehma0e45f32015-05-30 13:20:35 -0700101 // Maximum number of trailing zeroes before the decimal point before
102 // we switch to scientific notation with positive exponent.
103 private static final int SCI_NOTATION_EXTRA = 1;
104 // Extra digits for standard scientific notation. In this case we
Hans Boehm80018c82015-08-02 16:59:07 -0700105 // have a decimal point and no ellipsis.
106 // We assume that we do not drop digits to make room for the decimal
107 // point in ordinary scientific notation. Thus >= 1.
Hans Boehm1176f232015-05-11 16:26:03 -0700108 private ActionMode mActionMode;
109 private final ForegroundColorSpan mExponentColorSpan;
Hans Boehm84614952014-11-25 18:46:17 -0800110
111 public CalculatorResult(Context context, AttributeSet attrs) {
112 super(context, attrs);
113 mScroller = new OverScroller(context);
114 mGestureDetector = new GestureDetector(context,
115 new GestureDetector.SimpleOnGestureListener() {
116 @Override
Justin Klaassend48b7562015-04-16 16:51:38 -0700117 public boolean onDown(MotionEvent e) {
118 return true;
119 }
120 @Override
Hans Boehm84614952014-11-25 18:46:17 -0800121 public boolean onFling(MotionEvent e1, MotionEvent e2,
122 float velocityX, float velocityY) {
123 if (!mScroller.isFinished()) {
124 mCurrentPos = mScroller.getFinalX();
125 }
126 mScroller.forceFinished(true);
Hans Boehm1176f232015-05-11 16:26:03 -0700127 stopActionMode();
Hans Boehmfbcef702015-04-27 18:07:47 -0700128 CalculatorResult.this.cancelLongPress();
129 // Ignore scrolls of error string, etc.
130 if (!mScrollable) return true;
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700131 mScroller.fling(mCurrentPos, 0, - (int) velocityX, 0 /* horizontal only */,
Hans Boehm61568a12015-05-18 18:25:41 -0700132 mMinPos, mMaxPos, 0, 0);
Justin Klaassen44595162015-05-28 17:55:20 -0700133 postInvalidateOnAnimation();
Hans Boehm84614952014-11-25 18:46:17 -0800134 return true;
135 }
136 @Override
137 public boolean onScroll(MotionEvent e1, MotionEvent e2,
138 float distanceX, float distanceY) {
Hans Boehm61568a12015-05-18 18:25:41 -0700139 int distance = (int)distanceX;
Hans Boehm84614952014-11-25 18:46:17 -0800140 if (!mScroller.isFinished()) {
141 mCurrentPos = mScroller.getFinalX();
142 }
143 mScroller.forceFinished(true);
Hans Boehm1176f232015-05-11 16:26:03 -0700144 stopActionMode();
Hans Boehm84614952014-11-25 18:46:17 -0800145 CalculatorResult.this.cancelLongPress();
146 if (!mScrollable) return true;
Hans Boehm61568a12015-05-18 18:25:41 -0700147 if (mCurrentPos + distance < mMinPos) {
148 distance = mMinPos - mCurrentPos;
149 } else if (mCurrentPos + distance > mMaxPos) {
150 distance = mMaxPos - mCurrentPos;
151 }
Hans Boehm84614952014-11-25 18:46:17 -0800152 int duration = (int)(e2.getEventTime() - e1.getEventTime());
153 if (duration < 1 || duration > 100) duration = 10;
Hans Boehm61568a12015-05-18 18:25:41 -0700154 mScroller.startScroll(mCurrentPos, 0, distance, 0, (int)duration);
Justin Klaassen44595162015-05-28 17:55:20 -0700155 postInvalidateOnAnimation();
Hans Boehm84614952014-11-25 18:46:17 -0800156 return true;
157 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700158 @Override
159 public void onLongPress(MotionEvent e) {
Hans Boehm1176f232015-05-11 16:26:03 -0700160 if (mValid) {
161 mActionMode = startActionMode(mCopyActionModeCallback,
162 ActionMode.TYPE_FLOATING);
163 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700164 }
Hans Boehm84614952014-11-25 18:46:17 -0800165 });
166 setOnTouchListener(mTouchListener);
167 setHorizontallyScrolling(false); // do it ourselves
168 setCursorVisible(false);
Hans Boehm1176f232015-05-11 16:26:03 -0700169 mExponentColorSpan = new ForegroundColorSpan(
170 context.getColor(R.color.display_result_exponent_text_color));
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700171
172 // Copy ActionMode is triggered explicitly, not through
173 // setCustomSelectionActionModeCallback.
Hans Boehm84614952014-11-25 18:46:17 -0800174 }
175
176 void setEvaluator(Evaluator evaluator) {
177 mEvaluator = evaluator;
178 }
179
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700180 @Override
181 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
182 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
183
Justin Klaassen44595162015-05-28 17:55:20 -0700184 final TextPaint paint = getPaint();
Hans Boehm80018c82015-08-02 16:59:07 -0700185 final Context context = getContext();
Justin Klaassen44595162015-05-28 17:55:20 -0700186 final float newCharWidth = Layout.getDesiredWidth("\u2007", paint);
Hans Boehm80018c82015-08-02 16:59:07 -0700187 // Digits are presumed to have no more than newCharWidth.
188 // We sometimes replace a character by an ellipsis or, due to SCI_NOTATION_EXTRA, add
189 // an extra decimal separator beyond the maximum number of characters we normally allow.
190 // Empirically, our minus sign is also slightly wider than a digit, so we have to
191 // account for that. We never have both an ellipsis and two minus signs, and
192 // we assume an ellipsis is no narrower than a minus sign.
193 final float decimalSeparatorWidth = Layout.getDesiredWidth(
194 context.getString(R.string.dec_point), paint);
195 final float minusExtraWidth = Layout.getDesiredWidth(
196 context.getString(R.string.op_sub), paint) - newCharWidth;
197 final float ellipsisExtraWidth = Layout.getDesiredWidth(KeyMaps.ELLIPSIS, paint)
198 - newCharWidth;
199 final int extraWidth = (int) (Math.ceil(Math.max(decimalSeparatorWidth + minusExtraWidth,
200 ellipsisExtraWidth)) + Math.max(minusExtraWidth, 0.0f));
201 final int newWidthConstraint = MeasureSpec.getSize(widthMeasureSpec)
202 - (getPaddingLeft() + getPaddingRight()) - extraWidth;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700203 synchronized(mWidthLock) {
Hans Boehm013969e2015-04-13 20:29:47 -0700204 mWidthConstraint = newWidthConstraint;
205 mCharWidth = newCharWidth;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700206 }
207 }
208
Hans Boehma0e45f32015-05-30 13:20:35 -0700209 // Return the length of the exponent representation for the given exponent, in
210 // characters.
211 private final int expLen(int exp) {
212 if (exp == 0) return 0;
Hans Boehm5e802f32015-06-22 17:18:52 -0700213 final int abs_exp_digits = (int) Math.ceil(Math.log10(Math.abs((double)exp))
214 + 0.0000000001d /* Round whole numbers to next integer */);
215 return abs_exp_digits + (exp >= 0 ? 1 : 2);
Hans Boehm61568a12015-05-18 18:25:41 -0700216 }
217
Hans Boehma0e45f32015-05-30 13:20:35 -0700218 /**
219 * Initiate display of a new result.
220 * The parameters specify various properties of the result.
221 * @param initPrec Initial display precision computed by evaluator. (1 = tenths digit)
222 * @param msd Position of most significant digit. Offset from left of string.
223 Evaluator.INVALID_MSD if unknown.
224 * @param leastDigPos Position of least significant digit (1 = tenths digit)
225 * or Integer.MAX_VALUE.
226 * @param truncatedWholePart Result up to but not including decimal point.
227 Currently we only use the length.
228 */
229 void displayResult(int initPrec, int msd, int leastDigPos, String truncatedWholePart) {
230 initPositions(initPrec, msd, leastDigPos, truncatedWholePart);
Hans Boehm84614952014-11-25 18:46:17 -0800231 redisplay();
232 }
233
Hans Boehma0e45f32015-05-30 13:20:35 -0700234 /**
Hans Boehm5e802f32015-06-22 17:18:52 -0700235 * Set up scroll bounds (mMinPos, mMaxPos, etc.) and determine whether the result is
236 * scrollable, based on the supplied information about the result.
Hans Boehma0e45f32015-05-30 13:20:35 -0700237 * This is unfortunately complicated because we need to predict whether trailing digits
238 * will eventually be replaced by an exponent.
239 * Just appending the exponent during formatting would be simpler, but would produce
240 * jumpier results during transitions.
241 */
Hans Boehm5e802f32015-06-22 17:18:52 -0700242 private void initPositions(int initPrecOffset, int msdIndex, int lsdOffset,
243 String truncatedWholePart) {
Hans Boehma0e45f32015-05-30 13:20:35 -0700244 float charWidth;
245 int maxChars = getMaxChars();
246 mLastPos = INVALID;
Hans Boehm5e802f32015-06-22 17:18:52 -0700247 mLsdOffset = lsdOffset;
Hans Boehma0e45f32015-05-30 13:20:35 -0700248 synchronized(mWidthLock) {
249 charWidth = mCharWidth;
250 }
Hans Boehm5e802f32015-06-22 17:18:52 -0700251 mCurrentPos = mMinPos = (int) Math.round(initPrecOffset * charWidth);
Hans Boehma0e45f32015-05-30 13:20:35 -0700252 // Prevent scrolling past initial position, which is calculated to show leading digits.
Hans Boehm5e802f32015-06-22 17:18:52 -0700253 if (msdIndex == Evaluator.INVALID_MSD) {
Hans Boehma0e45f32015-05-30 13:20:35 -0700254 // Possible zero value
Hans Boehm5e802f32015-06-22 17:18:52 -0700255 if (lsdOffset == Integer.MIN_VALUE) {
Hans Boehma0e45f32015-05-30 13:20:35 -0700256 // Definite zero value.
257 mMaxPos = mMinPos;
Hans Boehm5e802f32015-06-22 17:18:52 -0700258 mMaxCharOffset = (int) Math.round(mMaxPos/charWidth);
Hans Boehma0e45f32015-05-30 13:20:35 -0700259 mScrollable = false;
260 } else {
261 // May be very small nonzero value. Allow user to find out.
Hans Boehm5e802f32015-06-22 17:18:52 -0700262 mMaxPos = mMaxCharOffset = MAX_RIGHT_SCROLL;
263 mMinPos -= charWidth; // Allow for future minus sign.
Hans Boehma0e45f32015-05-30 13:20:35 -0700264 mScrollable = true;
265 }
266 return;
267 }
268 int wholeLen = truncatedWholePart.length();
269 int negative = truncatedWholePart.charAt(0) == '-' ? 1 : 0;
Hans Boehm5e802f32015-06-22 17:18:52 -0700270 if (msdIndex > wholeLen && msdIndex <= wholeLen + 3) {
271 // Avoid tiny negative exponent; pretend msdIndex is just to the right of decimal point.
272 msdIndex = wholeLen - 1;
Hans Boehma0e45f32015-05-30 13:20:35 -0700273 }
Hans Boehm5e802f32015-06-22 17:18:52 -0700274 int minCharOffset = msdIndex - wholeLen;
Hans Boehma0e45f32015-05-30 13:20:35 -0700275 // Position of leftmost significant digit relative to dec. point.
276 // Usually negative.
Hans Boehm5e802f32015-06-22 17:18:52 -0700277 mMaxCharOffset = MAX_RIGHT_SCROLL; // How far does it make sense to scroll right?
Hans Boehma0e45f32015-05-30 13:20:35 -0700278 // If msd is left of decimal point should logically be
279 // mMinPos = - (int) Math.ceil(getPaint().measureText(truncatedWholePart)), but
280 // we eventually translate to a character position by dividing by mCharWidth.
281 // To avoid rounding issues, we use the analogous computation here.
Hans Boehm5e802f32015-06-22 17:18:52 -0700282 if (minCharOffset > -1 && minCharOffset < MAX_LEADING_ZEROES + 2) {
Hans Boehma0e45f32015-05-30 13:20:35 -0700283 // Small number of leading zeroes, avoid scientific notation.
Hans Boehm5e802f32015-06-22 17:18:52 -0700284 minCharOffset = -1;
Hans Boehma0e45f32015-05-30 13:20:35 -0700285 }
Hans Boehm5e802f32015-06-22 17:18:52 -0700286 if (lsdOffset < MAX_RIGHT_SCROLL) {
287 mMaxCharOffset = lsdOffset;
288 if (mMaxCharOffset < -1 && mMaxCharOffset > -(MAX_TRAILING_ZEROES + 2)) {
289 mMaxCharOffset = -1;
Hans Boehma0e45f32015-05-30 13:20:35 -0700290 }
Hans Boehm5e802f32015-06-22 17:18:52 -0700291 // lsdOffset is positive or negative, never 0.
292 int currentExpLen = 0; // Length of required standard scientific notation exponent.
293 if (mMaxCharOffset < -1) {
294 currentExpLen = expLen(-minCharOffset - 1);
295 } else if (minCharOffset > -1 || mMaxCharOffset >= maxChars) {
Hans Boehma0e45f32015-05-30 13:20:35 -0700296 // Number either entirely to the right of decimal point, or decimal point not
297 // visible when scrolled to the right.
Hans Boehm5e802f32015-06-22 17:18:52 -0700298 currentExpLen = expLen(-minCharOffset);
Hans Boehma0e45f32015-05-30 13:20:35 -0700299 }
Hans Boehm5e802f32015-06-22 17:18:52 -0700300 mScrollable = (mMaxCharOffset + currentExpLen - minCharOffset + negative >= maxChars);
301 int newMaxCharOffset;
302 if (currentExpLen > 0) {
303 if (mScrollable) {
304 // We'll use exponent corresponding to leastDigPos when scrolled to right.
305 newMaxCharOffset = mMaxCharOffset + expLen(-lsdOffset);
306 } else {
307 newMaxCharOffset = mMaxCharOffset + currentExpLen;
308 }
309 if (mMaxCharOffset <= -1 && newMaxCharOffset > -1) {
310 // Very unlikely; just drop exponent.
311 mMaxCharOffset = -1;
312 } else {
313 mMaxCharOffset = newMaxCharOffset;
Hans Boehma0e45f32015-05-30 13:20:35 -0700314 }
315 }
Hans Boehm5e802f32015-06-22 17:18:52 -0700316 mMaxPos = Math.min((int) Math.round(mMaxCharOffset * charWidth), MAX_RIGHT_SCROLL);
Hans Boehma0e45f32015-05-30 13:20:35 -0700317 if (!mScrollable) {
318 // Position the number consistently with our assumptions to make sure it
319 // actually fits.
320 mCurrentPos = mMaxPos;
321 }
322 } else {
Hans Boehm5e802f32015-06-22 17:18:52 -0700323 mMaxPos = mMaxCharOffset = MAX_RIGHT_SCROLL;
Hans Boehma0e45f32015-05-30 13:20:35 -0700324 mScrollable = true;
325 }
326 }
327
Hans Boehm84614952014-11-25 18:46:17 -0800328 void displayError(int resourceId) {
Hans Boehm760a9dc2015-04-20 10:27:12 -0700329 mValid = true;
Hans Boehm84614952014-11-25 18:46:17 -0800330 mScrollable = false;
331 setText(resourceId);
332 }
333
Hans Boehm013969e2015-04-13 20:29:47 -0700334 private final int MAX_COPY_SIZE = 1000000;
335
Hans Boehma0e45f32015-05-30 13:20:35 -0700336 /*
337 * Return the most significant digit position in the given string or Evaluator.INVALID_MSD.
Hans Boehm3666e632015-07-27 18:33:12 -0700338 * Unlike Evaluator.getMsdIndexOf, we treat a final 1 as significant.
Hans Boehma0e45f32015-05-30 13:20:35 -0700339 */
Hans Boehm3666e632015-07-27 18:33:12 -0700340 public static int getNaiveMsdIndexOf(String s) {
Hans Boehma0e45f32015-05-30 13:20:35 -0700341 int len = s.length();
Hans Boehma0e45f32015-05-30 13:20:35 -0700342 for (int i = 0; i < len; ++i) {
343 char c = s.charAt(i);
344 if (c != '-' && c != '.' && c != '0') {
345 return i;
346 }
347 }
348 return Evaluator.INVALID_MSD;
349 }
350
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700351 // Format a result returned by Evaluator.getString() into a single line containing ellipses
Hans Boehm3666e632015-07-27 18:33:12 -0700352 // (if appropriate) and an exponent (if appropriate). precOffset is the value that was passed
353 // to getString and thus identifies the significance of the rightmost digit.
Hans Boehma0e45f32015-05-30 13:20:35 -0700354 // A value of 1 means the rightmost digits corresponds to tenths.
355 // maxDigs is the maximum number of characters in the result.
Hans Boehm5e802f32015-06-22 17:18:52 -0700356 // We set lastDisplayedOffset[0] to the offset of the last digit actually appearing in
Hans Boehmf6dae112015-06-18 17:57:50 -0700357 // the display.
358 // If forcePrecision is true, we make sure that the last displayed digit corresponds to
Hans Boehm3666e632015-07-27 18:33:12 -0700359 // precOffset, and allow maxDigs to be exceeded in assing the exponent.
Hans Boehm08e8f322015-04-21 13:18:38 -0700360 // We add two distinct kinds of exponents:
Hans Boehm5e802f32015-06-22 17:18:52 -0700361 // (1) If the final result contains the leading digit we use standard scientific notation.
362 // (2) If not, we add an exponent corresponding to an interpretation of the final result as
363 // an integer.
Hans Boehm08e8f322015-04-21 13:18:38 -0700364 // We add an ellipsis on the left if the result was truncated.
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700365 // We add ellipses and exponents in a way that leaves most digits in the position they
366 // would have been in had we not done so.
367 // This minimizes jumps as a result of scrolling. Result is NOT internationalized,
Hans Boehm0b9806f2015-06-29 16:07:15 -0700368 // uses "E" for exponent.
Hans Boehm5e802f32015-06-22 17:18:52 -0700369 public String formatResult(String in, int precOffset, int maxDigs, boolean truncated,
370 boolean negative, int lastDisplayedOffset[], boolean forcePrecision) {
371 final int minusSpace = negative ? 1 : 0;
Hans Boehm3666e632015-07-27 18:33:12 -0700372 final int msdIndex = truncated ? -1 : getNaiveMsdIndexOf(in); // INVALID_MSD is OK.
Hans Boehm5e802f32015-06-22 17:18:52 -0700373 final int decIndex = in.indexOf('.');
374 String result = in;
375 lastDisplayedOffset[0] = precOffset;
376 if ((decIndex == -1 || msdIndex != Evaluator.INVALID_MSD
377 && msdIndex - decIndex > MAX_LEADING_ZEROES + 1) && precOffset != -1) {
Hans Boehma0e45f32015-05-30 13:20:35 -0700378 // No decimal point displayed, and it's not just to the right of the last digit,
379 // or we should suppress leading zeroes.
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700380 // Add an exponent to let the user track which digits are currently displayed.
Hans Boehm5e802f32015-06-22 17:18:52 -0700381 // Start with type (2) exponent if we dropped no digits. -1 accounts for decimal point.
382 final int initExponent = precOffset > 0 ? -precOffset : -precOffset - 1;
383 int exponent = initExponent;
Hans Boehm08e8f322015-04-21 13:18:38 -0700384 boolean hasPoint = false;
Hans Boehm5e802f32015-06-22 17:18:52 -0700385 if (!truncated && msdIndex < maxDigs - 1
386 && result.length() - msdIndex + 1 + minusSpace
387 <= maxDigs + SCI_NOTATION_EXTRA) {
388 // Type (1) exponent computation and transformation:
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700389 // Leading digit is in display window. Use standard calculator scientific notation
390 // with one digit to the left of the decimal point. Insert decimal point and
391 // delete leading zeroes.
Hans Boehma0e45f32015-05-30 13:20:35 -0700392 // We try to keep leading digits roughly in position, and never
Hans Boehmf6dae112015-06-18 17:57:50 -0700393 // lengthen the result by more than SCI_NOTATION_EXTRA.
Hans Boehm5e802f32015-06-22 17:18:52 -0700394 final int resLen = result.length();
395 String fraction = result.substring(msdIndex + 1, resLen);
396 result = (negative ? "-" : "") + result.substring(msdIndex, msdIndex + 1)
397 + "." + fraction;
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700398 // Original exp was correct for decimal point at right of fraction.
399 // Adjust by length of fraction.
Hans Boehm5e802f32015-06-22 17:18:52 -0700400 exponent = initExponent + resLen - msdIndex - 1;
Hans Boehm08e8f322015-04-21 13:18:38 -0700401 hasPoint = true;
402 }
Hans Boehm5e802f32015-06-22 17:18:52 -0700403 if (exponent != 0 || truncated) {
Hans Boehm08e8f322015-04-21 13:18:38 -0700404 // Actually add the exponent of either type:
Hans Boehmf6dae112015-06-18 17:57:50 -0700405 if (!forcePrecision) {
Hans Boehm5e802f32015-06-22 17:18:52 -0700406 int dropDigits; // Digits to drop to make room for exponent.
407 if (hasPoint) {
408 // Type (1) exponent.
Hans Boehmf6dae112015-06-18 17:57:50 -0700409 // Drop digits even if there is room. Otherwise the scrolling gets jumpy.
Hans Boehm5e802f32015-06-22 17:18:52 -0700410 dropDigits = expLen(exponent);
411 if (dropDigits >= result.length() - 1) {
412 // Jumpy is better than no mantissa. Probably impossible anyway.
413 dropDigits = Math.max(result.length() - 2, 0);
Hans Boehmf6dae112015-06-18 17:57:50 -0700414 }
Hans Boehm5e802f32015-06-22 17:18:52 -0700415 } else {
416 // Type (2) exponent.
417 // Exponent depends on the number of digits we drop, which depends on
418 // exponent ...
419 for (dropDigits = 2; expLen(initExponent + dropDigits) > dropDigits;
420 ++dropDigits) {}
421 exponent = initExponent + dropDigits;
422 if (precOffset - dropDigits > mLsdOffset) {
Hans Boehmf6dae112015-06-18 17:57:50 -0700423 // This can happen if e.g. result = 10^40 + 10^10
424 // It turns out we would otherwise display ...10e9 because it takes
425 // the same amount of space as ...1e10 but shows one more digit.
426 // But we don't want to display a trailing zero, even if it's free.
427 ++dropDigits;
Hans Boehm5e802f32015-06-22 17:18:52 -0700428 ++exponent;
Hans Boehmf6dae112015-06-18 17:57:50 -0700429 }
Hans Boehma0e45f32015-05-30 13:20:35 -0700430 }
Hans Boehm5e802f32015-06-22 17:18:52 -0700431 result = result.substring(0, result.length() - dropDigits);
432 lastDisplayedOffset[0] -= dropDigits;
Hans Boehm08e8f322015-04-21 13:18:38 -0700433 }
Hans Boehm0b9806f2015-06-29 16:07:15 -0700434 result = result + "E" + Integer.toString(exponent);
Hans Boehm08e8f322015-04-21 13:18:38 -0700435 } // else don't add zero exponent
436 }
Hans Boehm5e802f32015-06-22 17:18:52 -0700437 if (truncated || negative && result.charAt(0) != '-') {
438 result = KeyMaps.ELLIPSIS + result.substring(1, result.length());
439 }
440 return result;
Hans Boehm08e8f322015-04-21 13:18:38 -0700441 }
442
Hans Boehmf6dae112015-06-18 17:57:50 -0700443 /**
444 * Get formatted, but not internationalized, result from mEvaluator.
Hans Boehm5e802f32015-06-22 17:18:52 -0700445 * @param precOffset requested position (1 = tenths) of last included digit.
Hans Boehmf6dae112015-06-18 17:57:50 -0700446 * @param maxSize Maximum number of characters (more or less) in result.
Hans Boehm5e802f32015-06-22 17:18:52 -0700447 * @param lastDisplayedOffset Zeroth entry is set to actual offset of last included digit,
448 * after adjusting for exponent, etc.
Hans Boehmf6dae112015-06-18 17:57:50 -0700449 * @param forcePrecision Ensure that last included digit is at pos, at the expense
450 * of treating maxSize as a soft limit.
451 */
Hans Boehm5e802f32015-06-22 17:18:52 -0700452 private String getFormattedResult(int precOffset, int maxSize, int lastDisplayedOffset[],
Hans Boehmf6dae112015-06-18 17:57:50 -0700453 boolean forcePrecision) {
Hans Boehm08e8f322015-04-21 13:18:38 -0700454 final boolean truncated[] = new boolean[1];
455 final boolean negative[] = new boolean[1];
Hans Boehm5e802f32015-06-22 17:18:52 -0700456 final int requestedPrecOffset[] = {precOffset};
457 final String rawResult = mEvaluator.getString(requestedPrecOffset, mMaxCharOffset,
Hans Boehma0e45f32015-05-30 13:20:35 -0700458 maxSize, truncated, negative);
Hans Boehm5e802f32015-06-22 17:18:52 -0700459 return formatResult(rawResult, requestedPrecOffset[0], maxSize, truncated[0], negative[0],
460 lastDisplayedOffset, forcePrecision);
Hans Boehm08e8f322015-04-21 13:18:38 -0700461 }
462
Hans Boehm84614952014-11-25 18:46:17 -0800463 // Return entire result (within reason) up to current displayed precision.
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700464 public String getFullText() {
Hans Boehm760a9dc2015-04-20 10:27:12 -0700465 if (!mValid) return "";
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700466 if (!mScrollable) return getText().toString();
Hans Boehm5e802f32015-06-22 17:18:52 -0700467 int currentCharOffset = getCurrentCharOffset();
Hans Boehmf6dae112015-06-18 17:57:50 -0700468 int unused[] = new int[1];
Hans Boehm5e802f32015-06-22 17:18:52 -0700469 return KeyMaps.translateResult(getFormattedResult(mLastDisplayedOffset, MAX_COPY_SIZE,
Hans Boehmf6dae112015-06-18 17:57:50 -0700470 unused, true));
Hans Boehm84614952014-11-25 18:46:17 -0800471 }
472
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700473 public boolean fullTextIsExact() {
Hans Boehmf6dae112015-06-18 17:57:50 -0700474 return !mScrollable
Hans Boehm5e802f32015-06-22 17:18:52 -0700475 || mMaxCharOffset == getCurrentCharOffset() && mMaxCharOffset != MAX_RIGHT_SCROLL;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700476 }
477
Hans Boehm61568a12015-05-18 18:25:41 -0700478 /**
479 * Return the maximum number of characters that will fit in the result display.
480 * May be called asynchronously from non-UI thread.
481 */
Hans Boehm84614952014-11-25 18:46:17 -0800482 int getMaxChars() {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700483 int result;
484 synchronized(mWidthLock) {
Justin Klaassen44595162015-05-28 17:55:20 -0700485 result = (int) Math.floor(mWidthConstraint / mCharWidth);
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700486 // We can apparently finish evaluating before onMeasure in CalculatorText has been
487 // called, in which case we get 0 or -1 as the width constraint.
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700488 }
Hans Boehm84614952014-11-25 18:46:17 -0800489 if (result <= 0) {
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700490 // Return something conservatively big, to force sufficient evaluation.
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700491 return MAX_WIDTH;
Hans Boehm84614952014-11-25 18:46:17 -0800492 } else {
Hans Boehm80018c82015-08-02 16:59:07 -0700493 return result;
Hans Boehm84614952014-11-25 18:46:17 -0800494 }
495 }
496
Hans Boehm61568a12015-05-18 18:25:41 -0700497 /**
Justin Klaassen44595162015-05-28 17:55:20 -0700498 * @return {@code true} if the currently displayed result is scrollable
Hans Boehm61568a12015-05-18 18:25:41 -0700499 */
Justin Klaassen44595162015-05-28 17:55:20 -0700500 public boolean isScrollable() {
501 return mScrollable;
Hans Boehm61568a12015-05-18 18:25:41 -0700502 }
503
Hans Boehm5e802f32015-06-22 17:18:52 -0700504 int getCurrentCharOffset() {
Hans Boehm013969e2015-04-13 20:29:47 -0700505 synchronized(mWidthLock) {
Hans Boehma0e45f32015-05-30 13:20:35 -0700506 return (int) Math.round(mCurrentPos / mCharWidth);
Hans Boehm013969e2015-04-13 20:29:47 -0700507 }
508 }
509
Hans Boehm84614952014-11-25 18:46:17 -0800510 void clear() {
Hans Boehm760a9dc2015-04-20 10:27:12 -0700511 mValid = false;
Hans Boehm1176f232015-05-11 16:26:03 -0700512 mScrollable = false;
Hans Boehm84614952014-11-25 18:46:17 -0800513 setText("");
514 }
515
516 void redisplay() {
Hans Boehm5e802f32015-06-22 17:18:52 -0700517 int currentCharOffset = getCurrentCharOffset();
Hans Boehm84614952014-11-25 18:46:17 -0800518 int maxChars = getMaxChars();
Hans Boehm5e802f32015-06-22 17:18:52 -0700519 int lastDisplayedOffset[] = new int[1];
520 String result = getFormattedResult(currentCharOffset, maxChars, lastDisplayedOffset, false);
Hans Boehm0b9806f2015-06-29 16:07:15 -0700521 int expIndex = result.indexOf('E');
Hans Boehm013969e2015-04-13 20:29:47 -0700522 result = KeyMaps.translateResult(result);
Hans Boehm5e802f32015-06-22 17:18:52 -0700523 if (expIndex > 0 && result.indexOf('.') == -1) {
Hans Boehm84614952014-11-25 18:46:17 -0800524 // Gray out exponent if used as position indicator
525 SpannableString formattedResult = new SpannableString(result);
Hans Boehm5e802f32015-06-22 17:18:52 -0700526 formattedResult.setSpan(mExponentColorSpan, expIndex, result.length(),
Hans Boehm84614952014-11-25 18:46:17 -0800527 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
528 setText(formattedResult);
529 } else {
530 setText(result);
531 }
Hans Boehm5e802f32015-06-22 17:18:52 -0700532 mLastDisplayedOffset = lastDisplayedOffset[0];
Hans Boehm760a9dc2015-04-20 10:27:12 -0700533 mValid = true;
Hans Boehm84614952014-11-25 18:46:17 -0800534 }
535
536 @Override
537 public void computeScroll() {
538 if (!mScrollable) return;
539 if (mScroller.computeScrollOffset()) {
540 mCurrentPos = mScroller.getCurrX();
541 if (mCurrentPos != mLastPos) {
542 mLastPos = mCurrentPos;
543 redisplay();
544 }
545 if (!mScroller.isFinished()) {
Justin Klaassen44595162015-05-28 17:55:20 -0700546 postInvalidateOnAnimation();
Hans Boehm84614952014-11-25 18:46:17 -0800547 }
548 }
549 }
550
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700551 // Copy support:
552
Hans Boehm7f83e362015-06-10 15:41:04 -0700553 private ActionMode.Callback2 mCopyActionModeCallback = new ActionMode.Callback2() {
554
555 private BackgroundColorSpan mHighlightSpan;
556
557 private void highlightResult() {
558 final Spannable text = (Spannable) getText();
559 mHighlightSpan = new BackgroundColorSpan(getHighlightColor());
560 text.setSpan(mHighlightSpan, 0, text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
561 }
562
563 private void unhighlightResult() {
564 final Spannable text = (Spannable) getText();
565 text.removeSpan(mHighlightSpan);
566 }
567
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700568 @Override
569 public boolean onCreateActionMode(ActionMode mode, Menu menu) {
570 MenuInflater inflater = mode.getMenuInflater();
571 inflater.inflate(R.menu.copy, menu);
Hans Boehm7f83e362015-06-10 15:41:04 -0700572 highlightResult();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700573 return true;
574 }
575
576 @Override
577 public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
578 return false; // Return false if nothing is done
579 }
580
581 @Override
582 public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
583 switch (item.getItemId()) {
584 case R.id.menu_copy:
585 copyContent();
586 mode.finish();
587 return true;
588 default:
589 return false;
590 }
591 }
592
593 @Override
594 public void onDestroyActionMode(ActionMode mode) {
Hans Boehm7f83e362015-06-10 15:41:04 -0700595 unhighlightResult();
Hans Boehm1176f232015-05-11 16:26:03 -0700596 mActionMode = null;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700597 }
Hans Boehm7f83e362015-06-10 15:41:04 -0700598
599 @Override
600 public void onGetContentRect(ActionMode mode, View view, Rect outRect) {
601 super.onGetContentRect(mode, view, outRect);
602 outRect.left += getPaddingLeft();
603 outRect.top += getPaddingTop();
604 outRect.right -= getPaddingRight();
605 outRect.bottom -= getPaddingBottom();
606 final int width = (int) Layout.getDesiredWidth(getText(), getPaint());
607 if (width < outRect.width()) {
608 outRect.left = outRect.right - width;
609 }
610 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700611 };
612
Hans Boehm1176f232015-05-11 16:26:03 -0700613 public boolean stopActionMode() {
614 if (mActionMode != null) {
615 mActionMode.finish();
616 return true;
617 }
618 return false;
619 }
620
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700621 private void setPrimaryClip(ClipData clip) {
622 ClipboardManager clipboard = (ClipboardManager) getContext().
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700623 getSystemService(Context.CLIPBOARD_SERVICE);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700624 clipboard.setPrimaryClip(clip);
625 }
626
627 private void copyContent() {
628 final CharSequence text = getFullText();
629 ClipboardManager clipboard =
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700630 (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
631 // We include a tag URI, to allow us to recognize our own results and handle them
632 // specially.
633 ClipData.Item newItem = new ClipData.Item(text, null, mEvaluator.capture());
634 String[] mimeTypes = new String[] {ClipDescription.MIMETYPE_TEXT_PLAIN};
635 ClipData cd = new ClipData("calculator result", mimeTypes, newItem);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700636 clipboard.setPrimaryClip(cd);
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700637 Toast.makeText(getContext(), R.string.text_copied_toast, Toast.LENGTH_SHORT).show();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700638 }
639
Hans Boehm84614952014-11-25 18:46:17 -0800640}