blob: c569d4c243fa45b9a87d8bd4aa83ccf23975051f [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.ClipboardManager;
20import android.content.ClipData;
21import android.content.ClipDescription;
22import android.content.Context;
Hans Boehm84614952014-11-25 18:46:17 -080023import android.graphics.Typeface;
24import android.graphics.Paint;
25import android.graphics.Rect;
26import android.graphics.Color;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070027import android.net.Uri;
28import android.widget.TextView;
Hans Boehm84614952014-11-25 18:46:17 -080029import android.widget.OverScroller;
Hans Boehm84614952014-11-25 18:46:17 -080030import android.text.Editable;
Hans Boehm84614952014-11-25 18:46:17 -080031import android.text.SpannableString;
Hans Boehm1176f232015-05-11 16:26:03 -070032import android.text.Spanned;
Hans Boehm84614952014-11-25 18:46:17 -080033import android.text.style.ForegroundColorSpan;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070034import android.util.AttributeSet;
35import android.util.Log;
36import android.view.ActionMode;
37import android.view.GestureDetector;
38import android.view.Menu;
39import android.view.MenuInflater;
40import android.view.MenuItem;
41import android.view.MotionEvent;
42import android.view.View;
43import android.widget.Toast;
Hans Boehm84614952014-11-25 18:46:17 -080044
45import android.support.v4.view.ViewCompat;
46
47
48// A text widget that is "infinitely" scrollable to the right,
49// and obtains the text to display via a callback to Logic.
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070050public class CalculatorResult extends TextView {
Hans Boehm08e8f322015-04-21 13:18:38 -070051 static final int MAX_RIGHT_SCROLL = 100000000;
52 static final int INVALID = MAX_RIGHT_SCROLL + 10000;
Hans Boehm84614952014-11-25 18:46:17 -080053 // A larger value is unlikely to avoid running out of space
54 final OverScroller mScroller;
55 final GestureDetector mGestureDetector;
56 class MyTouchListener implements View.OnTouchListener {
57 @Override
58 public boolean onTouch(View v, MotionEvent event) {
59 boolean res = mGestureDetector.onTouchEvent(event);
60 return res;
61 }
62 }
63 final MyTouchListener mTouchListener = new MyTouchListener();
64 private Evaluator mEvaluator;
65 private boolean mScrollable = false;
66 // A scrollable result is currently displayed.
Hans Boehm760a9dc2015-04-20 10:27:12 -070067 private boolean mValid = false;
68 // The result holds something valid; either a
69 // a number or an error message.
Hans Boehm84614952014-11-25 18:46:17 -080070 private int mCurrentPos;// Position of right of display relative
71 // to decimal point, in pixels.
72 // Large positive values mean the decimal
73 // point is scrolled off the left of the
74 // display. Zero means decimal point is
75 // barely displayed on the right.
76 private int mLastPos; // Position already reflected in display.
77 private int mMinPos; // Maximum position before all digits
78 // digits disappear of the right.
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070079 private Object mWidthLock = new Object();
80 // Protects the next two fields.
81 private int mWidthConstraint = -1;
82 // Our total width in pixels.
83 private int mCharWidth = 1;
84 // Maximum character width.
85 // For now we pretend that all characters
86 // have this width.
87 // TODO: We're not really using a fixed
88 // width font. But it appears to be close
89 // enough for the characters we use that
90 // the difference is not noticeable.
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070091 private static final int MAX_WIDTH = 100;
92 // Maximum number of digits displayed
Hans Boehm1176f232015-05-11 16:26:03 -070093 private ActionMode mActionMode;
94 private final ForegroundColorSpan mExponentColorSpan;
Hans Boehm84614952014-11-25 18:46:17 -080095
96 public CalculatorResult(Context context, AttributeSet attrs) {
97 super(context, attrs);
98 mScroller = new OverScroller(context);
99 mGestureDetector = new GestureDetector(context,
100 new GestureDetector.SimpleOnGestureListener() {
101 @Override
Justin Klaassend48b7562015-04-16 16:51:38 -0700102 public boolean onDown(MotionEvent e) {
103 return true;
104 }
105 @Override
Hans Boehm84614952014-11-25 18:46:17 -0800106 public boolean onFling(MotionEvent e1, MotionEvent e2,
107 float velocityX, float velocityY) {
108 if (!mScroller.isFinished()) {
109 mCurrentPos = mScroller.getFinalX();
110 }
111 mScroller.forceFinished(true);
Hans Boehm1176f232015-05-11 16:26:03 -0700112 stopActionMode();
Hans Boehmfbcef702015-04-27 18:07:47 -0700113 CalculatorResult.this.cancelLongPress();
114 // Ignore scrolls of error string, etc.
115 if (!mScrollable) return true;
Hans Boehm84614952014-11-25 18:46:17 -0800116 mScroller.fling(mCurrentPos, 0, - (int) velocityX,
117 0 /* horizontal only */, mMinPos,
118 MAX_RIGHT_SCROLL, 0, 0);
119 ViewCompat.postInvalidateOnAnimation(CalculatorResult.this);
120 return true;
121 }
122 @Override
123 public boolean onScroll(MotionEvent e1, MotionEvent e2,
124 float distanceX, float distanceY) {
125 // TODO: Should we be dealing with any edge effects here?
126 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;
133 int duration = (int)(e2.getEventTime() - e1.getEventTime());
134 if (duration < 1 || duration > 100) duration = 10;
135 mScroller.startScroll(mCurrentPos, 0, (int)distanceX, 0,
136 (int)duration);
137 ViewCompat.postInvalidateOnAnimation(CalculatorResult.this);
138 return true;
139 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700140 @Override
141 public void onLongPress(MotionEvent e) {
Hans Boehm1176f232015-05-11 16:26:03 -0700142 if (mValid) {
143 mActionMode = startActionMode(mCopyActionModeCallback,
144 ActionMode.TYPE_FLOATING);
145 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700146 }
Hans Boehm84614952014-11-25 18:46:17 -0800147 });
148 setOnTouchListener(mTouchListener);
149 setHorizontallyScrolling(false); // do it ourselves
150 setCursorVisible(false);
Hans Boehm1176f232015-05-11 16:26:03 -0700151 mExponentColorSpan = new ForegroundColorSpan(
152 context.getColor(R.color.display_result_exponent_text_color));
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700153
154 // Copy ActionMode is triggered explicitly, not through
155 // setCustomSelectionActionModeCallback.
Hans Boehm84614952014-11-25 18:46:17 -0800156 }
157
158 void setEvaluator(Evaluator evaluator) {
159 mEvaluator = evaluator;
160 }
161
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700162 @Override
163 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
164 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
165
Hans Boehm013969e2015-04-13 20:29:47 -0700166 char testChar = KeyMaps.translateResult("5").charAt(0);
167 // TODO: Redo on Locale change? Doesn't seem to matter?
168 // We try to determine the maximal size of a digit plus
169 // corresponding inter-character space.
170 // We assume that "5" has maximal width. Since any
171 // string includes one fewer inter-character space than
172 // characters, me measure one that's longer than any real
173 // display string, and then divide by the number of characters.
174 // This should bound the per-character space we need for any
175 // real string.
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700176 StringBuilder sb = new StringBuilder(MAX_WIDTH);
177 for (int i = 0; i < MAX_WIDTH; ++i) {
Hans Boehm013969e2015-04-13 20:29:47 -0700178 sb.append(testChar);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700179 }
Hans Boehm013969e2015-04-13 20:29:47 -0700180 final int newWidthConstraint =
181 MeasureSpec.getSize(widthMeasureSpec)
182 - getPaddingLeft() - getPaddingRight();
183 final int newCharWidth =
Hans Boehmc5e6e152015-04-22 12:06:29 -0700184 (int)Math.ceil(getPaint().measureText(sb.toString())
185 / MAX_WIDTH);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700186 synchronized(mWidthLock) {
Hans Boehm013969e2015-04-13 20:29:47 -0700187 mWidthConstraint = newWidthConstraint;
188 mCharWidth = newCharWidth;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700189 }
190 }
191
Hans Boehm84614952014-11-25 18:46:17 -0800192 // Display a new result, given initial displayed
193 // precision and the string representing the whole part of
194 // the number to be displayed.
195 // We pass the string, instead of just the length, so we have
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700196 // one less place to fix in case we ever decide to
197 // correctly use a variable width font.
Hans Boehm84614952014-11-25 18:46:17 -0800198 void displayResult(int initPrec, String truncatedWholePart) {
199 mLastPos = INVALID;
Hans Boehm013969e2015-04-13 20:29:47 -0700200 synchronized(mWidthLock) {
201 mCurrentPos = initPrec * mCharWidth;
202 }
Hans Boehmc5e6e152015-04-22 12:06:29 -0700203 mMinPos = - (int) Math.ceil(getPaint().measureText(truncatedWholePart));
Hans Boehm84614952014-11-25 18:46:17 -0800204 redisplay();
205 }
206
Hans Boehm84614952014-11-25 18:46:17 -0800207 void displayError(int resourceId) {
Hans Boehm760a9dc2015-04-20 10:27:12 -0700208 mValid = true;
Hans Boehm84614952014-11-25 18:46:17 -0800209 mScrollable = false;
210 setText(resourceId);
211 }
212
Hans Boehm013969e2015-04-13 20:29:47 -0700213 private final int MAX_COPY_SIZE = 1000000;
214
Hans Boehm08e8f322015-04-21 13:18:38 -0700215 // Format a result returned by Evaluator.getString() into a single
216 // line containing ellipses (if appropriate) and an exponent
217 // (if appropriate). digs is the value that was passed to
218 // getString and thus identifies the significance of the
219 // rightmost digit.
220 // We add two distinct kinds of exponents:
221 // 1) If the final result contains the leading digit we use standard
222 // scientific notation.
223 // 2) If not, we add an exponent corresponding to an interpretation
224 // of the final result as an integer.
225 // We add an ellipsis on the left if the result was truncated.
226 // We add ellipses and exponents in a way that leaves most digits
227 // in the position they would have been in had we not done so.
228 // This minimizes jumps as a result of scrolling.
229 // Result is NOT internationalized, uses "e" for exponent.
230 // last_included[0] is set to the position of the last digit we
231 // actually include; thus caller can tell whether result is exact.
232 public String formatResult(String res, int digs,
233 int maxDigs, boolean truncated,
234 boolean negative) {
235 if (truncated) {
236 res = KeyMaps.ELLIPSIS + res.substring(1, res.length());
237 }
238 int decIndex = res.indexOf('.');
239 int resLen = res.length();
240 if (decIndex == -1 && digs != -1) {
241 // No decimal point displayed, and it's not just
242 // to the right of the last digit.
243 // Add an exponent to let the user track which
244 // digits are currently displayed.
245 // This is a bit tricky, since the number of displayed
246 // digits affects the displayed exponent, which can
247 // affect the room we have for mantissa digits.
248 // We occasionally display one digit too few.
249 // This is sometimes unavoidable, but we could
250 // avoid it in more cases.
251 int exp = digs > 0 ? -digs : -digs - 1;
252 // Can be used as TYPE (2) EXPONENT.
253 // -1 accounts for decimal point.
254 int msd; // Position of most significant digit in res
255 // or indication its outside res.
256 boolean hasPoint = false;
257 if (truncated) {
258 msd = -1;
259 } else {
260 msd = Evaluator.getMsdPos(res); // INVALID_MSD is OK
261 }
262 if (msd < maxDigs - 1 && msd >= 0) {
263 // TYPE (1) EXPONENT computation and transformation:
264 // Leading digit is in display window.
265 // Use standard calculator scientific notation
266 // with one digit to the left of the decimal point.
267 // Insert decimal point and delete leading zeroes.
268 String fraction = res.substring(msd + 1, resLen);
269 res = (negative ? "-" : "")
270 + res.substring(msd, msd+1) + "." + fraction;
271 exp += resLen - msd - 1;
272 // Original exp was correct for decimal point at right
273 // of fraction. Adjust by length of fraction.
274 resLen = res.length();
275 hasPoint = true;
276 }
277 if (exp != 0 || truncated) {
278 // Actually add the exponent of either type:
279 String expAsString = Integer.toString(exp);
280 int expDigits = expAsString.length();
281 int dropDigits = expDigits + 1;
282 // Drop digits even if there is room.
283 // Otherwise the scrolling gets jumpy.
284 if (dropDigits >= resLen - 1) {
285 dropDigits = Math.max(resLen - 2, 0);
286 // Jumpy is better than no mantissa.
287 }
288 if (!hasPoint) {
289 // Special handling for TYPE(2) EXPONENT:
290 exp += dropDigits;
291 expAsString = Integer.toString(exp);
292 // Adjust for digits we are about to drop
293 // to drop to make room for exponent.
294 // This can affect the room we have for the
295 // mantissa. We adjust only for positive exponents,
296 // when it could otherwise result in a truncated
297 // displayed result.
298 if (exp > 0 && expAsString.length() > expDigits) {
299 // ++expDigits; (dead code)
300 ++dropDigits;
301 ++exp;
302 // This cannot increase the length a second time.
303 }
304 }
305 res = res.substring(0, resLen - dropDigits);
306 res = res + "e" + expAsString;
307 } // else don't add zero exponent
308 }
309 return res;
310 }
311
312 // Get formatted, but not internationalized, result from
313 // mEvaluator.
314 private String getFormattedResult(int pos, int maxSize) {
315 final boolean truncated[] = new boolean[1];
316 final boolean negative[] = new boolean[1];
317 final int requested_prec[] = {pos};
318 final String raw_res = mEvaluator.getString(requested_prec, maxSize,
319 truncated, negative);
320 return formatResult(raw_res, requested_prec[0], maxSize,
321 truncated[0], negative[0]);
322 }
323
Hans Boehm84614952014-11-25 18:46:17 -0800324 // Return entire result (within reason) up to current displayed precision.
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700325 public String getFullText() {
Hans Boehm760a9dc2015-04-20 10:27:12 -0700326 if (!mValid) return "";
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700327 if (!mScrollable) return getText().toString();
Hans Boehm013969e2015-04-13 20:29:47 -0700328 int currentCharPos = getCurrentCharPos();
329 return KeyMaps.translateResult(
Hans Boehm08e8f322015-04-21 13:18:38 -0700330 getFormattedResult(currentCharPos, MAX_COPY_SIZE));
Hans Boehm84614952014-11-25 18:46:17 -0800331 }
332
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700333 public boolean fullTextIsExact() {
334 BoundedRational rat = mEvaluator.getRational();
Hans Boehm013969e2015-04-13 20:29:47 -0700335 int currentCharPos = getCurrentCharPos();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700336 if (currentCharPos == -1) {
337 // Suppressing decimal point; still showing all
338 // integral digits.
339 currentCharPos = 0;
340 }
341 // TODO: Could handle scientific notation cases better;
342 // We currently treat those conservatively as approximate.
343 return (currentCharPos >= BoundedRational.digitsRequired(rat));
344 }
345
346 // May be called asynchronously from non-UI thread.
Hans Boehm84614952014-11-25 18:46:17 -0800347 int getMaxChars() {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700348 // We only use 2/3 of the available space, since the
349 // left 1/3 of the result is not visible when it is shown
350 // in large size.
351 int result;
352 synchronized(mWidthLock) {
353 result = 2 * mWidthConstraint / (3 * mCharWidth);
354 // We can apparently finish evaluating before
Hans Boehm08e8f322015-04-21 13:18:38 -0700355 // onMeasure in CalculatorText has been called, in
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700356 // which case we get 0 or -1 as the width constraint.
357 }
Hans Boehm84614952014-11-25 18:46:17 -0800358 if (result <= 0) {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700359 // Return something conservatively big, to force sufficient
360 // evaluation.
361 return MAX_WIDTH;
Hans Boehm84614952014-11-25 18:46:17 -0800362 } else {
363 return result;
364 }
365 }
366
Hans Boehm013969e2015-04-13 20:29:47 -0700367 int getCurrentCharPos() {
368 synchronized(mWidthLock) {
369 return mCurrentPos/mCharWidth;
370 }
371 }
372
Hans Boehm84614952014-11-25 18:46:17 -0800373 void clear() {
Hans Boehm760a9dc2015-04-20 10:27:12 -0700374 mValid = false;
Hans Boehm1176f232015-05-11 16:26:03 -0700375 mScrollable = false;
Hans Boehm84614952014-11-25 18:46:17 -0800376 setText("");
377 }
378
379 void redisplay() {
Hans Boehm013969e2015-04-13 20:29:47 -0700380 int currentCharPos = getCurrentCharPos();
Hans Boehm84614952014-11-25 18:46:17 -0800381 int maxChars = getMaxChars();
Hans Boehm08e8f322015-04-21 13:18:38 -0700382 String result = getFormattedResult(currentCharPos, maxChars);
Hans Boehm84614952014-11-25 18:46:17 -0800383 int epos = result.indexOf('e');
Hans Boehm013969e2015-04-13 20:29:47 -0700384 result = KeyMaps.translateResult(result);
Hans Boehm84614952014-11-25 18:46:17 -0800385 if (epos > 0 && result.indexOf('.') == -1) {
386 // Gray out exponent if used as position indicator
387 SpannableString formattedResult = new SpannableString(result);
Hans Boehm1176f232015-05-11 16:26:03 -0700388 formattedResult.setSpan(mExponentColorSpan, epos, result.length(),
Hans Boehm84614952014-11-25 18:46:17 -0800389 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
390 setText(formattedResult);
391 } else {
392 setText(result);
393 }
Hans Boehm760a9dc2015-04-20 10:27:12 -0700394 mValid = true;
Hans Boehm84614952014-11-25 18:46:17 -0800395 mScrollable = true;
396 }
397
398 @Override
399 public void computeScroll() {
400 if (!mScrollable) return;
401 if (mScroller.computeScrollOffset()) {
402 mCurrentPos = mScroller.getCurrX();
403 if (mCurrentPos != mLastPos) {
404 mLastPos = mCurrentPos;
405 redisplay();
406 }
407 if (!mScroller.isFinished()) {
408 ViewCompat.postInvalidateOnAnimation(this);
409 }
410 }
411 }
412
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700413 // Copy support:
414
415 private ActionMode.Callback mCopyActionModeCallback =
416 new ActionMode.Callback() {
417 @Override
418 public boolean onCreateActionMode(ActionMode mode, Menu menu) {
419 MenuInflater inflater = mode.getMenuInflater();
420 inflater.inflate(R.menu.copy, menu);
421 return true;
422 }
423
424 @Override
425 public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
426 return false; // Return false if nothing is done
427 }
428
429 @Override
430 public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
431 switch (item.getItemId()) {
432 case R.id.menu_copy:
433 copyContent();
434 mode.finish();
435 return true;
436 default:
437 return false;
438 }
439 }
440
441 @Override
442 public void onDestroyActionMode(ActionMode mode) {
Hans Boehm1176f232015-05-11 16:26:03 -0700443 mActionMode = null;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700444 }
445 };
446
Hans Boehm1176f232015-05-11 16:26:03 -0700447 public boolean stopActionMode() {
448 if (mActionMode != null) {
449 mActionMode.finish();
450 return true;
451 }
452 return false;
453 }
454
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700455 private void setPrimaryClip(ClipData clip) {
456 ClipboardManager clipboard = (ClipboardManager) getContext().
457 getSystemService(Context.CLIPBOARD_SERVICE);
458 clipboard.setPrimaryClip(clip);
459 }
460
461 private void copyContent() {
462 final CharSequence text = getFullText();
463 ClipboardManager clipboard =
464 (ClipboardManager) getContext().getSystemService(
465 Context.CLIPBOARD_SERVICE);
466 // We include a tag URI, to allow us to recognize our
467 // own results and handle them specially.
468 ClipData.Item newItem = new ClipData.Item(text, null,
469 mEvaluator.capture());
470 String[] mimeTypes =
471 new String[] {ClipDescription.MIMETYPE_TEXT_PLAIN};
472 ClipData cd = new ClipData("calculator result",
473 mimeTypes, newItem);
474 clipboard.setPrimaryClip(cd);
475 Toast.makeText(getContext(), R.string.text_copied_toast,
476 Toast.LENGTH_SHORT).show();
477 }
478
Hans Boehm84614952014-11-25 18:46:17 -0800479}