blob: 279c75e0db4cf15d6ef0d43f7ce44a21a627f506 [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;
31import android.text.Spanned;
32import android.text.SpannableString;
33import 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 Boehm84614952014-11-25 18:46:17 -080051 final static int MAX_RIGHT_SCROLL = 100000000;
52 final static int INVALID = MAX_RIGHT_SCROLL + 10000;
53 // 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 Boehm84614952014-11-25 18:46:17 -080091 private Paint mPaint; // Paint object matching display.
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070092 private static final int MAX_WIDTH = 100;
93 // Maximum number of digits displayed
Hans Boehm84614952014-11-25 18:46:17 -080094
95 public CalculatorResult(Context context, AttributeSet attrs) {
96 super(context, attrs);
97 mScroller = new OverScroller(context);
98 mGestureDetector = new GestureDetector(context,
99 new GestureDetector.SimpleOnGestureListener() {
100 @Override
Justin Klaassend48b7562015-04-16 16:51:38 -0700101 public boolean onDown(MotionEvent e) {
102 return true;
103 }
104 @Override
Hans Boehm84614952014-11-25 18:46:17 -0800105 public boolean onFling(MotionEvent e1, MotionEvent e2,
106 float velocityX, float velocityY) {
107 if (!mScroller.isFinished()) {
108 mCurrentPos = mScroller.getFinalX();
109 }
110 mScroller.forceFinished(true);
111 CalculatorResult.this.cancelLongPress(); // Ignore scrolls of error string, etc.
112 if (!mScrollable) return true;
113 mScroller.fling(mCurrentPos, 0, - (int) velocityX,
114 0 /* horizontal only */, mMinPos,
115 MAX_RIGHT_SCROLL, 0, 0);
116 ViewCompat.postInvalidateOnAnimation(CalculatorResult.this);
117 return true;
118 }
119 @Override
120 public boolean onScroll(MotionEvent e1, MotionEvent e2,
121 float distanceX, float distanceY) {
122 // TODO: Should we be dealing with any edge effects here?
123 if (!mScroller.isFinished()) {
124 mCurrentPos = mScroller.getFinalX();
125 }
126 mScroller.forceFinished(true);
127 CalculatorResult.this.cancelLongPress();
128 if (!mScrollable) return true;
129 int duration = (int)(e2.getEventTime() - e1.getEventTime());
130 if (duration < 1 || duration > 100) duration = 10;
131 mScroller.startScroll(mCurrentPos, 0, (int)distanceX, 0,
132 (int)duration);
133 ViewCompat.postInvalidateOnAnimation(CalculatorResult.this);
134 return true;
135 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700136 @Override
137 public void onLongPress(MotionEvent e) {
138 startActionMode(mCopyActionModeCallback);
139 }
Hans Boehm84614952014-11-25 18:46:17 -0800140 });
141 setOnTouchListener(mTouchListener);
142 setHorizontallyScrolling(false); // do it ourselves
143 setCursorVisible(false);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700144
145 // Copy ActionMode is triggered explicitly, not through
146 // setCustomSelectionActionModeCallback.
Hans Boehm84614952014-11-25 18:46:17 -0800147 }
148
149 void setEvaluator(Evaluator evaluator) {
150 mEvaluator = evaluator;
151 }
152
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700153 @Override
154 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
155 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
156
157 mPaint = getPaint();
Hans Boehm013969e2015-04-13 20:29:47 -0700158 char testChar = KeyMaps.translateResult("5").charAt(0);
159 // TODO: Redo on Locale change? Doesn't seem to matter?
160 // We try to determine the maximal size of a digit plus
161 // corresponding inter-character space.
162 // We assume that "5" has maximal width. Since any
163 // string includes one fewer inter-character space than
164 // characters, me measure one that's longer than any real
165 // display string, and then divide by the number of characters.
166 // This should bound the per-character space we need for any
167 // real string.
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700168 StringBuilder sb = new StringBuilder(MAX_WIDTH);
169 for (int i = 0; i < MAX_WIDTH; ++i) {
Hans Boehm013969e2015-04-13 20:29:47 -0700170 sb.append(testChar);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700171 }
Hans Boehm013969e2015-04-13 20:29:47 -0700172 final int newWidthConstraint =
173 MeasureSpec.getSize(widthMeasureSpec)
174 - getPaddingLeft() - getPaddingRight();
175 final int newCharWidth =
176 (int)Math.ceil(mPaint.measureText(sb.toString()) / MAX_WIDTH);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700177 synchronized(mWidthLock) {
Hans Boehm013969e2015-04-13 20:29:47 -0700178 mWidthConstraint = newWidthConstraint;
179 mCharWidth = newCharWidth;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700180 }
181 }
182
Hans Boehm84614952014-11-25 18:46:17 -0800183 // Display a new result, given initial displayed
184 // precision and the string representing the whole part of
185 // the number to be displayed.
186 // We pass the string, instead of just the length, so we have
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700187 // one less place to fix in case we ever decide to
188 // correctly use a variable width font.
Hans Boehm84614952014-11-25 18:46:17 -0800189 void displayResult(int initPrec, String truncatedWholePart) {
190 mLastPos = INVALID;
Hans Boehm013969e2015-04-13 20:29:47 -0700191 synchronized(mWidthLock) {
192 mCurrentPos = initPrec * mCharWidth;
193 }
Hans Boehm84614952014-11-25 18:46:17 -0800194 mMinPos = - (int) Math.ceil(mPaint.measureText(truncatedWholePart));
195 redisplay();
196 }
197
Hans Boehm84614952014-11-25 18:46:17 -0800198 void displayError(int resourceId) {
Hans Boehm760a9dc2015-04-20 10:27:12 -0700199 mValid = true;
Hans Boehm84614952014-11-25 18:46:17 -0800200 mScrollable = false;
201 setText(resourceId);
202 }
203
Hans Boehm013969e2015-04-13 20:29:47 -0700204 private final int MAX_COPY_SIZE = 1000000;
205
Hans Boehm84614952014-11-25 18:46:17 -0800206 // Return entire result (within reason) up to current displayed precision.
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700207 public String getFullText() {
Hans Boehm760a9dc2015-04-20 10:27:12 -0700208 if (!mValid) return "";
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700209 if (!mScrollable) return getText().toString();
Hans Boehm013969e2015-04-13 20:29:47 -0700210 int currentCharPos = getCurrentCharPos();
211 return KeyMaps.translateResult(
212 mEvaluator.getString(currentCharPos, MAX_COPY_SIZE));
Hans Boehm84614952014-11-25 18:46:17 -0800213 }
214
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700215 public boolean fullTextIsExact() {
216 BoundedRational rat = mEvaluator.getRational();
Hans Boehm013969e2015-04-13 20:29:47 -0700217 int currentCharPos = getCurrentCharPos();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700218 if (currentCharPos == -1) {
219 // Suppressing decimal point; still showing all
220 // integral digits.
221 currentCharPos = 0;
222 }
223 // TODO: Could handle scientific notation cases better;
224 // We currently treat those conservatively as approximate.
225 return (currentCharPos >= BoundedRational.digitsRequired(rat));
226 }
227
228 // May be called asynchronously from non-UI thread.
Hans Boehm84614952014-11-25 18:46:17 -0800229 int getMaxChars() {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700230 // We only use 2/3 of the available space, since the
231 // left 1/3 of the result is not visible when it is shown
232 // in large size.
233 int result;
234 synchronized(mWidthLock) {
235 result = 2 * mWidthConstraint / (3 * mCharWidth);
236 // We can apparently finish evaluating before
237 // onMeasure in CalculatorEditText has been called, in
238 // which case we get 0 or -1 as the width constraint.
239 }
Hans Boehm84614952014-11-25 18:46:17 -0800240 if (result <= 0) {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700241 // Return something conservatively big, to force sufficient
242 // evaluation.
243 return MAX_WIDTH;
Hans Boehm84614952014-11-25 18:46:17 -0800244 } else {
245 return result;
246 }
247 }
248
Hans Boehm013969e2015-04-13 20:29:47 -0700249 int getCurrentCharPos() {
250 synchronized(mWidthLock) {
251 return mCurrentPos/mCharWidth;
252 }
253 }
254
Hans Boehm84614952014-11-25 18:46:17 -0800255 void clear() {
Hans Boehm760a9dc2015-04-20 10:27:12 -0700256 mValid = false;
Hans Boehm84614952014-11-25 18:46:17 -0800257 setText("");
258 }
259
260 void redisplay() {
Hans Boehm013969e2015-04-13 20:29:47 -0700261 int currentCharPos = getCurrentCharPos();
Hans Boehm84614952014-11-25 18:46:17 -0800262 int maxChars = getMaxChars();
263 String result = mEvaluator.getString(currentCharPos, maxChars);
264 int epos = result.indexOf('e');
Hans Boehm013969e2015-04-13 20:29:47 -0700265 result = KeyMaps.translateResult(result);
Hans Boehm84614952014-11-25 18:46:17 -0800266 if (epos > 0 && result.indexOf('.') == -1) {
267 // Gray out exponent if used as position indicator
268 SpannableString formattedResult = new SpannableString(result);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700269 formattedResult.setSpan(new ForegroundColorSpan(Color.LTGRAY),
Hans Boehm84614952014-11-25 18:46:17 -0800270 epos, result.length(),
271 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
272 setText(formattedResult);
273 } else {
274 setText(result);
275 }
Hans Boehm760a9dc2015-04-20 10:27:12 -0700276 mValid = true;
Hans Boehm84614952014-11-25 18:46:17 -0800277 mScrollable = true;
278 }
279
280 @Override
281 public void computeScroll() {
282 if (!mScrollable) return;
283 if (mScroller.computeScrollOffset()) {
284 mCurrentPos = mScroller.getCurrX();
285 if (mCurrentPos != mLastPos) {
286 mLastPos = mCurrentPos;
287 redisplay();
288 }
289 if (!mScroller.isFinished()) {
290 ViewCompat.postInvalidateOnAnimation(this);
291 }
292 }
293 }
294
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700295 // Copy support:
296
297 private ActionMode.Callback mCopyActionModeCallback =
298 new ActionMode.Callback() {
299 @Override
300 public boolean onCreateActionMode(ActionMode mode, Menu menu) {
301 MenuInflater inflater = mode.getMenuInflater();
302 inflater.inflate(R.menu.copy, menu);
303 return true;
304 }
305
306 @Override
307 public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
308 return false; // Return false if nothing is done
309 }
310
311 @Override
312 public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
313 switch (item.getItemId()) {
314 case R.id.menu_copy:
315 copyContent();
316 mode.finish();
317 return true;
318 default:
319 return false;
320 }
321 }
322
323 @Override
324 public void onDestroyActionMode(ActionMode mode) {
325 }
326 };
327
328 private void setPrimaryClip(ClipData clip) {
329 ClipboardManager clipboard = (ClipboardManager) getContext().
330 getSystemService(Context.CLIPBOARD_SERVICE);
331 clipboard.setPrimaryClip(clip);
332 }
333
334 private void copyContent() {
335 final CharSequence text = getFullText();
336 ClipboardManager clipboard =
337 (ClipboardManager) getContext().getSystemService(
338 Context.CLIPBOARD_SERVICE);
339 // We include a tag URI, to allow us to recognize our
340 // own results and handle them specially.
341 ClipData.Item newItem = new ClipData.Item(text, null,
342 mEvaluator.capture());
343 String[] mimeTypes =
344 new String[] {ClipDescription.MIMETYPE_TEXT_PLAIN};
345 ClipData cd = new ClipData("calculator result",
346 mimeTypes, newItem);
347 clipboard.setPrimaryClip(cd);
348 Toast.makeText(getContext(), R.string.text_copied_toast,
349 Toast.LENGTH_SHORT).show();
350 }
351
Hans Boehm84614952014-11-25 18:46:17 -0800352}