blob: fb26b5c4019c3e030d5ed61f9025538e22ced3cd [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.
67 private int mCurrentPos;// Position of right of display relative
68 // to decimal point, in pixels.
69 // Large positive values mean the decimal
70 // point is scrolled off the left of the
71 // display. Zero means decimal point is
72 // barely displayed on the right.
73 private int mLastPos; // Position already reflected in display.
74 private int mMinPos; // Maximum position before all digits
75 // digits disappear of the right.
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070076 private Object mWidthLock = new Object();
77 // Protects the next two fields.
78 private int mWidthConstraint = -1;
79 // Our total width in pixels.
80 private int mCharWidth = 1;
81 // Maximum character width.
82 // For now we pretend that all characters
83 // have this width.
84 // TODO: We're not really using a fixed
85 // width font. But it appears to be close
86 // enough for the characters we use that
87 // the difference is not noticeable.
Hans Boehm84614952014-11-25 18:46:17 -080088 private Paint mPaint; // Paint object matching display.
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070089 private static final int MAX_WIDTH = 100;
90 // Maximum number of digits displayed
Hans Boehm84614952014-11-25 18:46:17 -080091
92 public CalculatorResult(Context context, AttributeSet attrs) {
93 super(context, attrs);
94 mScroller = new OverScroller(context);
95 mGestureDetector = new GestureDetector(context,
96 new GestureDetector.SimpleOnGestureListener() {
97 @Override
98 public boolean onFling(MotionEvent e1, MotionEvent e2,
99 float velocityX, float velocityY) {
100 if (!mScroller.isFinished()) {
101 mCurrentPos = mScroller.getFinalX();
102 }
103 mScroller.forceFinished(true);
104 CalculatorResult.this.cancelLongPress(); // Ignore scrolls of error string, etc.
105 if (!mScrollable) return true;
106 mScroller.fling(mCurrentPos, 0, - (int) velocityX,
107 0 /* horizontal only */, mMinPos,
108 MAX_RIGHT_SCROLL, 0, 0);
109 ViewCompat.postInvalidateOnAnimation(CalculatorResult.this);
110 return true;
111 }
112 @Override
113 public boolean onScroll(MotionEvent e1, MotionEvent e2,
114 float distanceX, float distanceY) {
115 // TODO: Should we be dealing with any edge effects here?
116 if (!mScroller.isFinished()) {
117 mCurrentPos = mScroller.getFinalX();
118 }
119 mScroller.forceFinished(true);
120 CalculatorResult.this.cancelLongPress();
121 if (!mScrollable) return true;
122 int duration = (int)(e2.getEventTime() - e1.getEventTime());
123 if (duration < 1 || duration > 100) duration = 10;
124 mScroller.startScroll(mCurrentPos, 0, (int)distanceX, 0,
125 (int)duration);
126 ViewCompat.postInvalidateOnAnimation(CalculatorResult.this);
127 return true;
128 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700129 @Override
130 public void onLongPress(MotionEvent e) {
131 startActionMode(mCopyActionModeCallback);
132 }
Hans Boehm84614952014-11-25 18:46:17 -0800133 });
134 setOnTouchListener(mTouchListener);
135 setHorizontallyScrolling(false); // do it ourselves
136 setCursorVisible(false);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700137
138 // Copy ActionMode is triggered explicitly, not through
139 // setCustomSelectionActionModeCallback.
Hans Boehm84614952014-11-25 18:46:17 -0800140 }
141
142 void setEvaluator(Evaluator evaluator) {
143 mEvaluator = evaluator;
144 }
145
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700146 @Override
147 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
148 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
149
150 mPaint = getPaint();
151 // We assume that "5" has maximal width. We measure a
152 // long string to make sure that spaces are included.
153 StringBuilder sb = new StringBuilder(MAX_WIDTH);
154 for (int i = 0; i < MAX_WIDTH; ++i) {
155 sb.append('5');
156 }
157 synchronized(mWidthLock) {
158 mWidthConstraint = MeasureSpec.getSize(widthMeasureSpec)
159 - getPaddingLeft() - getPaddingRight();
160 mCharWidth = (int)Math.ceil(mPaint.measureText(sb.toString())
161 / MAX_WIDTH);
162 }
163 }
164
Hans Boehm84614952014-11-25 18:46:17 -0800165 // Display a new result, given initial displayed
166 // precision and the string representing the whole part of
167 // the number to be displayed.
168 // We pass the string, instead of just the length, so we have
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700169 // one less place to fix in case we ever decide to
170 // correctly use a variable width font.
Hans Boehm84614952014-11-25 18:46:17 -0800171 void displayResult(int initPrec, String truncatedWholePart) {
172 mLastPos = INVALID;
173 mCurrentPos = initPrec * mCharWidth;
174 mMinPos = - (int) Math.ceil(mPaint.measureText(truncatedWholePart));
175 redisplay();
176 }
177
Hans Boehm84614952014-11-25 18:46:17 -0800178 void displayError(int resourceId) {
179 mScrollable = false;
180 setText(resourceId);
181 }
182
183 // Return entire result (within reason) up to current displayed precision.
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700184 public String getFullText() {
185 if (!mScrollable) return getText().toString();
Hans Boehm84614952014-11-25 18:46:17 -0800186 int currentCharPos = mCurrentPos/mCharWidth;
187 return mEvaluator.getString(currentCharPos, 1000000);
188 }
189
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700190 public boolean fullTextIsExact() {
191 BoundedRational rat = mEvaluator.getRational();
192 int currentCharPos = mCurrentPos/mCharWidth;
193 if (currentCharPos == -1) {
194 // Suppressing decimal point; still showing all
195 // integral digits.
196 currentCharPos = 0;
197 }
198 // TODO: Could handle scientific notation cases better;
199 // We currently treat those conservatively as approximate.
200 return (currentCharPos >= BoundedRational.digitsRequired(rat));
201 }
202
203 // May be called asynchronously from non-UI thread.
Hans Boehm84614952014-11-25 18:46:17 -0800204 int getMaxChars() {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700205 // We only use 2/3 of the available space, since the
206 // left 1/3 of the result is not visible when it is shown
207 // in large size.
208 int result;
209 synchronized(mWidthLock) {
210 result = 2 * mWidthConstraint / (3 * mCharWidth);
211 // We can apparently finish evaluating before
212 // onMeasure in CalculatorEditText has been called, in
213 // which case we get 0 or -1 as the width constraint.
214 }
Hans Boehm84614952014-11-25 18:46:17 -0800215 if (result <= 0) {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700216 // Return something conservatively big, to force sufficient
217 // evaluation.
218 return MAX_WIDTH;
Hans Boehm84614952014-11-25 18:46:17 -0800219 } else {
220 return result;
221 }
222 }
223
224 void clear() {
225 setText("");
226 }
227
228 void redisplay() {
229 int currentCharPos = mCurrentPos/mCharWidth;
230 int maxChars = getMaxChars();
231 String result = mEvaluator.getString(currentCharPos, maxChars);
232 int epos = result.indexOf('e');
233 // TODO: Internationalization for decimal point?
234 if (epos > 0 && result.indexOf('.') == -1) {
235 // Gray out exponent if used as position indicator
236 SpannableString formattedResult = new SpannableString(result);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700237 formattedResult.setSpan(new ForegroundColorSpan(Color.LTGRAY),
Hans Boehm84614952014-11-25 18:46:17 -0800238 epos, result.length(),
239 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
240 setText(formattedResult);
241 } else {
242 setText(result);
243 }
244 mScrollable = true;
245 }
246
247 @Override
248 public void computeScroll() {
249 if (!mScrollable) return;
250 if (mScroller.computeScrollOffset()) {
251 mCurrentPos = mScroller.getCurrX();
252 if (mCurrentPos != mLastPos) {
253 mLastPos = mCurrentPos;
254 redisplay();
255 }
256 if (!mScroller.isFinished()) {
257 ViewCompat.postInvalidateOnAnimation(this);
258 }
259 }
260 }
261
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700262 // Copy support:
263
264 private ActionMode.Callback mCopyActionModeCallback =
265 new ActionMode.Callback() {
266 @Override
267 public boolean onCreateActionMode(ActionMode mode, Menu menu) {
268 MenuInflater inflater = mode.getMenuInflater();
269 inflater.inflate(R.menu.copy, menu);
270 return true;
271 }
272
273 @Override
274 public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
275 return false; // Return false if nothing is done
276 }
277
278 @Override
279 public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
280 switch (item.getItemId()) {
281 case R.id.menu_copy:
282 copyContent();
283 mode.finish();
284 return true;
285 default:
286 return false;
287 }
288 }
289
290 @Override
291 public void onDestroyActionMode(ActionMode mode) {
292 }
293 };
294
295 private void setPrimaryClip(ClipData clip) {
296 ClipboardManager clipboard = (ClipboardManager) getContext().
297 getSystemService(Context.CLIPBOARD_SERVICE);
298 clipboard.setPrimaryClip(clip);
299 }
300
301 private void copyContent() {
302 final CharSequence text = getFullText();
303 ClipboardManager clipboard =
304 (ClipboardManager) getContext().getSystemService(
305 Context.CLIPBOARD_SERVICE);
306 // We include a tag URI, to allow us to recognize our
307 // own results and handle them specially.
308 ClipData.Item newItem = new ClipData.Item(text, null,
309 mEvaluator.capture());
310 String[] mimeTypes =
311 new String[] {ClipDescription.MIMETYPE_TEXT_PLAIN};
312 ClipData cd = new ClipData("calculator result",
313 mimeTypes, newItem);
314 clipboard.setPrimaryClip(cd);
315 Toast.makeText(getContext(), R.string.text_copied_toast,
316 Toast.LENGTH_SHORT).show();
317 }
318
Hans Boehm84614952014-11-25 18:46:17 -0800319}