blob: 9cf23bbc419b31ff11de4ca92cba8bf6cd3b2277 [file] [log] [blame]
Justin Klaassen4b3af052014-05-27 17:53:10 -07001/*
Justin Klaassen12da1ad2016-04-04 14:20:37 -07002 * Copyright (C) 2016 The Android Open Source Project
Justin Klaassen4b3af052014-05-27 17:53:10 -07003 *
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
Hans Boehm013969e2015-04-13 20:29:47 -070017// TODO: Copy & more general paste in formula? Note that this requires
18// great care: Currently the text version of a displayed formula
19// is not directly useful for re-evaluating the formula later, since
20// it contains ellipses representing subexpressions evaluated with
21// a different degree mode. Rather than supporting copy from the
22// formula window, we may eventually want to support generation of a
23// more useful text version in a separate window. It's not clear
24// this is worth the added (code and user) complexity.
Hans Boehm84614952014-11-25 18:46:17 -080025
Justin Klaassen4b3af052014-05-27 17:53:10 -070026package com.android.calculator2;
27
28import android.animation.Animator;
Justin Klaassen5f2a3342014-06-11 17:40:22 -070029import android.animation.Animator.AnimatorListener;
Justin Klaassen4b3af052014-05-27 17:53:10 -070030import android.animation.AnimatorListenerAdapter;
31import android.animation.AnimatorSet;
Justin Klaassen4b3af052014-05-27 17:53:10 -070032import android.animation.ObjectAnimator;
Justin Klaassen44595162015-05-28 17:55:20 -070033import android.animation.PropertyValuesHolder;
Justin Klaassen9d33cdc2016-02-21 14:16:14 -080034import android.app.ActionBar;
Justin Klaassen4b3af052014-05-27 17:53:10 -070035import android.app.Activity;
Justin Klaassenfc5ac822015-06-18 13:15:17 -070036import android.content.ClipData;
Hans Boehm5e6a0ca2015-09-22 17:09:01 -070037import android.content.DialogInterface;
Justin Klaassend36d63e2015-05-05 12:59:36 -070038import android.content.Intent;
Hans Boehmbfe8c222015-04-02 16:26:07 -070039import android.content.res.Resources;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070040import android.graphics.Color;
Justin Klaassen8fff1442014-06-19 10:43:29 -070041import android.graphics.Rect;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070042import android.net.Uri;
Justin Klaassen4b3af052014-05-27 17:53:10 -070043import android.os.Bundle;
Justin Klaassenf79d6f62014-08-26 12:27:08 -070044import android.support.annotation.NonNull;
Chenjie Yu3937b652016-06-01 23:14:26 -070045import android.support.v4.content.ContextCompat;
Justin Klaassen3b4d13d2014-06-06 18:18:37 +010046import android.support.v4.view.ViewPager;
Annie Chine918fd22016-03-09 11:07:54 -080047import android.text.Editable;
Hans Boehm8a4f81c2015-07-09 10:41:25 -070048import android.text.SpannableStringBuilder;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070049import android.text.Spanned;
Annie Chinf360ef02016-03-10 13:45:39 -080050import android.text.TextUtils;
Annie Chine918fd22016-03-09 11:07:54 -080051import android.text.TextWatcher;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070052import android.text.style.ForegroundColorSpan;
Justin Klaassen44595162015-05-28 17:55:20 -070053import android.util.Property;
Annie Chine918fd22016-03-09 11:07:54 -080054import android.view.ActionMode;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070055import android.view.KeyCharacterMap;
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -070056import android.view.KeyEvent;
Hans Boehm84614952014-11-25 18:46:17 -080057import android.view.Menu;
58import android.view.MenuItem;
Justin Klaassen4b3af052014-05-27 17:53:10 -070059import android.view.View;
60import android.view.View.OnLongClickListener;
Justin Klaassen5f2a3342014-06-11 17:40:22 -070061import android.view.ViewAnimationUtils;
Justin Klaassen8fff1442014-06-19 10:43:29 -070062import android.view.ViewGroupOverlay;
Annie Chine918fd22016-03-09 11:07:54 -080063import android.view.ViewTreeObserver;
Justin Klaassen4b3af052014-05-27 17:53:10 -070064import android.view.animation.AccelerateDecelerateInterpolator;
Annie Chine918fd22016-03-09 11:07:54 -080065import android.widget.HorizontalScrollView;
Justin Klaassenfed941a2014-06-09 18:42:40 +010066import android.widget.TextView;
Justin Klaassend48b7562015-04-16 16:51:38 -070067import android.widget.Toolbar;
Justin Klaassenfed941a2014-06-09 18:42:40 +010068
Hans Boehm08e8f322015-04-21 13:18:38 -070069import com.android.calculator2.CalculatorText.OnTextSizeChangeListener;
Hans Boehm84614952014-11-25 18:46:17 -080070
71import java.io.ByteArrayInputStream;
Hans Boehm84614952014-11-25 18:46:17 -080072import java.io.ByteArrayOutputStream;
Hans Boehm84614952014-11-25 18:46:17 -080073import java.io.IOException;
Justin Klaassen721ec842015-05-28 14:30:08 -070074import java.io.ObjectInput;
75import java.io.ObjectInputStream;
76import java.io.ObjectOutput;
77import java.io.ObjectOutputStream;
Justin Klaassen4b3af052014-05-27 17:53:10 -070078
Justin Klaassen04f79c72014-06-27 17:25:35 -070079public class Calculator extends Activity
Hans Boehm5e6a0ca2015-09-22 17:09:01 -070080 implements OnTextSizeChangeListener, OnLongClickListener, CalculatorText.OnPasteListener,
81 AlertDialogFragment.OnClickListener {
Justin Klaassen2be4fdb2014-08-06 19:54:09 -070082
83 /**
84 * Constant for an invalid resource id.
85 */
86 public static final int INVALID_RES_ID = -1;
Justin Klaassen4b3af052014-05-27 17:53:10 -070087
88 private enum CalculatorState {
Hans Boehm84614952014-11-25 18:46:17 -080089 INPUT, // Result and formula both visible, no evaluation requested,
90 // Though result may be visible on bottom line.
91 EVALUATE, // Both visible, evaluation requested, evaluation/animation incomplete.
Hans Boehmc1ea0912015-06-19 15:05:07 -070092 // Not used for instant result evaluation.
Hans Boehm84614952014-11-25 18:46:17 -080093 INIT, // Very temporary state used as alternative to EVALUATE
94 // during reinitialization. Do not animate on completion.
95 ANIMATE, // Result computed, animation to enlarge result window in progress.
96 RESULT, // Result displayed, formula invisible.
97 // If we are in RESULT state, the formula was evaluated without
98 // error to initial precision.
99 ERROR // Error displayed: Formula visible, result shows error message.
100 // Display similar to INPUT state.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700101 }
Hans Boehm84614952014-11-25 18:46:17 -0800102 // Normal transition sequence is
103 // INPUT -> EVALUATE -> ANIMATE -> RESULT (or ERROR) -> INPUT
104 // A RESULT -> ERROR transition is possible in rare corner cases, in which
105 // a higher precision evaluation exposes an error. This is possible, since we
106 // initially evaluate assuming we were given a well-defined problem. If we
107 // were actually asked to compute sqrt(<extremely tiny negative number>) we produce 0
108 // unless we are asked for enough precision that we can distinguish the argument from zero.
109 // TODO: Consider further heuristics to reduce the chance of observing this?
110 // It already seems to be observable only in contrived cases.
111 // ANIMATE, ERROR, and RESULT are translated to an INIT state if the application
112 // is restarted in that state. This leads us to recompute and redisplay the result
113 // ASAP.
114 // TODO: Possibly save a bit more information, e.g. its initial display string
115 // or most significant digit position, to speed up restart.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700116
Justin Klaassen44595162015-05-28 17:55:20 -0700117 private final Property<TextView, Integer> TEXT_COLOR =
118 new Property<TextView, Integer>(Integer.class, "textColor") {
119 @Override
120 public Integer get(TextView textView) {
121 return textView.getCurrentTextColor();
122 }
123
124 @Override
125 public void set(TextView textView, Integer textColor) {
126 textView.setTextColor(textColor);
127 }
128 };
129
Justin Klaassen3e223ea2016-02-05 14:18:06 -0800130 private static final String NAME = "Calculator";
Hans Boehm84614952014-11-25 18:46:17 -0800131 private static final String KEY_DISPLAY_STATE = NAME + "_display_state";
Hans Boehm760a9dc2015-04-20 10:27:12 -0700132 private static final String KEY_UNPROCESSED_CHARS = NAME + "_unprocessed_chars";
Justin Klaassen3e223ea2016-02-05 14:18:06 -0800133 /**
134 * Associated value is a byte array holding the evaluator state.
135 */
Hans Boehm84614952014-11-25 18:46:17 -0800136 private static final String KEY_EVAL_STATE = NAME + "_eval_state";
Justin Klaassen3e223ea2016-02-05 14:18:06 -0800137 private static final String KEY_INVERSE_MODE = NAME + "_inverse_mode";
Christine Frankseeff27f2016-07-29 12:05:29 -0700138 /**
139 * Associated value is an boolean holding the visibility state of the toolbar.
140 */
141 private static final String KEY_SHOW_TOOLBAR = NAME + "_show_toolbar";
Justin Klaassen741471e2014-06-11 09:43:44 -0700142
Annie Chine918fd22016-03-09 11:07:54 -0800143 private final ViewTreeObserver.OnPreDrawListener mPreDrawListener =
144 new ViewTreeObserver.OnPreDrawListener() {
145 @Override
146 public boolean onPreDraw() {
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700147 mFormulaContainer.scrollTo(mFormulaText.getRight(), 0);
Annie Chine918fd22016-03-09 11:07:54 -0800148 final ViewTreeObserver observer = mFormulaContainer.getViewTreeObserver();
149 if (observer.isAlive()) {
150 observer.removeOnPreDrawListener(this);
151 }
152 return false;
153 }
154 };
155
156 private final TextWatcher mFormulaTextWatcher = new TextWatcher() {
157 @Override
158 public void beforeTextChanged(CharSequence charSequence, int start, int count, int after) {
159 }
160
161 @Override
162 public void onTextChanged(CharSequence charSequence, int start, int count, int after) {
163 }
164
165 @Override
166 public void afterTextChanged(Editable editable) {
167 final ViewTreeObserver observer = mFormulaContainer.getViewTreeObserver();
168 if (observer.isAlive()) {
169 observer.removeOnPreDrawListener(mPreDrawListener);
170 observer.addOnPreDrawListener(mPreDrawListener);
171 }
172 }
173 };
174
Justin Klaassen4b3af052014-05-27 17:53:10 -0700175 private CalculatorState mCurrentState;
Hans Boehm84614952014-11-25 18:46:17 -0800176 private Evaluator mEvaluator;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700177
Justin Klaassen9d33cdc2016-02-21 14:16:14 -0800178 private CalculatorDisplay mDisplayView;
Justin Klaassend48b7562015-04-16 16:51:38 -0700179 private TextView mModeView;
Hans Boehm08e8f322015-04-21 13:18:38 -0700180 private CalculatorText mFormulaText;
Justin Klaassen44595162015-05-28 17:55:20 -0700181 private CalculatorResult mResultText;
Annie Chine918fd22016-03-09 11:07:54 -0800182 private HorizontalScrollView mFormulaContainer;
Justin Klaassend48b7562015-04-16 16:51:38 -0700183
Justin Klaassen3b4d13d2014-06-06 18:18:37 +0100184 private ViewPager mPadViewPager;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700185 private View mDeleteButton;
186 private View mClearButton;
Justin Klaassend48b7562015-04-16 16:51:38 -0700187 private View mEqualButton;
Justin Klaassene2711cb2015-05-28 11:13:17 -0700188
189 private TextView mInverseToggle;
190 private TextView mModeToggle;
191
Justin Klaassen721ec842015-05-28 14:30:08 -0700192 private View[] mInvertibleButtons;
Justin Klaassene2711cb2015-05-28 11:13:17 -0700193 private View[] mInverseButtons;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700194
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700195 private View mCurrentButton;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700196 private Animator mCurrentAnimator;
197
Hans Boehm8a4f81c2015-07-09 10:41:25 -0700198 // Characters that were recently entered at the end of the display that have not yet
199 // been added to the underlying expression.
200 private String mUnprocessedChars = null;
201
202 // Color to highlight unprocessed characters from physical keyboard.
203 // TODO: should probably match this to the error color?
204 private ForegroundColorSpan mUnprocessedColorSpan = new ForegroundColorSpan(Color.RED);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700205
Annie Chin26e159e2016-05-18 15:17:14 -0700206 // Whether the display is one line.
207 private boolean mOneLine;
208
Justin Klaassen4b3af052014-05-27 17:53:10 -0700209 @Override
210 protected void onCreate(Bundle savedInstanceState) {
211 super.onCreate(savedInstanceState);
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700212 setContentView(R.layout.activity_calculator);
Justin Klaassend48b7562015-04-16 16:51:38 -0700213 setActionBar((Toolbar) findViewById(R.id.toolbar));
214
215 // Hide all default options in the ActionBar.
216 getActionBar().setDisplayOptions(0);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700217
Justin Klaassen9d33cdc2016-02-21 14:16:14 -0800218 // Ensure the toolbar stays visible while the options menu is displayed.
219 getActionBar().addOnMenuVisibilityListener(new ActionBar.OnMenuVisibilityListener() {
220 @Override
221 public void onMenuVisibilityChanged(boolean isVisible) {
222 mDisplayView.setForceToolbarVisible(isVisible);
223 }
224 });
225
226 mDisplayView = (CalculatorDisplay) findViewById(R.id.display);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700227 mModeView = (TextView) findViewById(R.id.mode);
Hans Boehm08e8f322015-04-21 13:18:38 -0700228 mFormulaText = (CalculatorText) findViewById(R.id.formula);
Justin Klaassen44595162015-05-28 17:55:20 -0700229 mResultText = (CalculatorResult) findViewById(R.id.result);
Annie Chine918fd22016-03-09 11:07:54 -0800230 mFormulaContainer = (HorizontalScrollView) findViewById(R.id.formula_container);
Justin Klaassend48b7562015-04-16 16:51:38 -0700231
Justin Klaassen3b4d13d2014-06-06 18:18:37 +0100232 mPadViewPager = (ViewPager) findViewById(R.id.pad_pager);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700233 mDeleteButton = findViewById(R.id.del);
234 mClearButton = findViewById(R.id.clr);
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700235 mEqualButton = findViewById(R.id.pad_numeric).findViewById(R.id.eq);
236 if (mEqualButton == null || mEqualButton.getVisibility() != View.VISIBLE) {
237 mEqualButton = findViewById(R.id.pad_operator).findViewById(R.id.eq);
238 }
Justin Klaassene2711cb2015-05-28 11:13:17 -0700239
240 mInverseToggle = (TextView) findViewById(R.id.toggle_inv);
241 mModeToggle = (TextView) findViewById(R.id.toggle_mode);
242
Annie Chin26e159e2016-05-18 15:17:14 -0700243 mOneLine = mResultText.getVisibility() == View.INVISIBLE;
244
Justin Klaassen721ec842015-05-28 14:30:08 -0700245 mInvertibleButtons = new View[] {
246 findViewById(R.id.fun_sin),
247 findViewById(R.id.fun_cos),
Hans Boehm4db31b42015-05-31 12:19:05 -0700248 findViewById(R.id.fun_tan),
249 findViewById(R.id.fun_ln),
250 findViewById(R.id.fun_log),
251 findViewById(R.id.op_sqrt)
Justin Klaassene2711cb2015-05-28 11:13:17 -0700252 };
253 mInverseButtons = new View[] {
254 findViewById(R.id.fun_arcsin),
255 findViewById(R.id.fun_arccos),
Hans Boehm4db31b42015-05-31 12:19:05 -0700256 findViewById(R.id.fun_arctan),
257 findViewById(R.id.fun_exp),
258 findViewById(R.id.fun_10pow),
259 findViewById(R.id.op_sqr)
Justin Klaassene2711cb2015-05-28 11:13:17 -0700260 };
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700261
Justin Klaassen44595162015-05-28 17:55:20 -0700262 mEvaluator = new Evaluator(this, mResultText);
263 mResultText.setEvaluator(mEvaluator);
Hans Boehm013969e2015-04-13 20:29:47 -0700264 KeyMaps.setActivity(this);
Justin Klaassen2be4fdb2014-08-06 19:54:09 -0700265
Hans Boehm84614952014-11-25 18:46:17 -0800266 if (savedInstanceState != null) {
267 setState(CalculatorState.values()[
268 savedInstanceState.getInt(KEY_DISPLAY_STATE,
269 CalculatorState.INPUT.ordinal())]);
Hans Boehm760a9dc2015-04-20 10:27:12 -0700270 CharSequence unprocessed = savedInstanceState.getCharSequence(KEY_UNPROCESSED_CHARS);
271 if (unprocessed != null) {
272 mUnprocessedChars = unprocessed.toString();
273 }
274 byte[] state = savedInstanceState.getByteArray(KEY_EVAL_STATE);
Hans Boehm84614952014-11-25 18:46:17 -0800275 if (state != null) {
276 try (ObjectInput in = new ObjectInputStream(new ByteArrayInputStream(state))) {
277 mEvaluator.restoreInstanceState(in);
278 } catch (Throwable ignored) {
279 // When in doubt, revert to clean state
280 mCurrentState = CalculatorState.INPUT;
281 mEvaluator.clear();
282 }
283 }
Hans Boehmfbcef702015-04-27 18:07:47 -0700284 } else {
285 mCurrentState = CalculatorState.INPUT;
286 mEvaluator.clear();
Hans Boehm84614952014-11-25 18:46:17 -0800287 }
Justin Klaassene2711cb2015-05-28 11:13:17 -0700288
Hans Boehm08e8f322015-04-21 13:18:38 -0700289 mFormulaText.setOnTextSizeChangeListener(this);
Justin Klaassenfc5ac822015-06-18 13:15:17 -0700290 mFormulaText.setOnPasteListener(this);
Annie Chine918fd22016-03-09 11:07:54 -0800291 mFormulaText.addTextChangedListener(mFormulaTextWatcher);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700292 mDeleteButton.setOnLongClickListener(this);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700293
Justin Klaassen3e223ea2016-02-05 14:18:06 -0800294 onInverseToggled(savedInstanceState != null
295 && savedInstanceState.getBoolean(KEY_INVERSE_MODE));
Christine Frankseeff27f2016-07-29 12:05:29 -0700296
Justin Klaassene2711cb2015-05-28 11:13:17 -0700297 onModeChanged(mEvaluator.getDegreeMode());
Christine Frankseeff27f2016-07-29 12:05:29 -0700298 if (savedInstanceState != null &&
299 savedInstanceState.getBoolean(KEY_SHOW_TOOLBAR, true) == false) {
300 mDisplayView.hideToolbar();
301 } else {
302 showAndMaybeHideToolbar();
303 }
Justin Klaassene2711cb2015-05-28 11:13:17 -0700304
Hans Boehm84614952014-11-25 18:46:17 -0800305 if (mCurrentState != CalculatorState.INPUT) {
Hans Boehmfbcef702015-04-27 18:07:47 -0700306 // Just reevaluate.
307 redisplayFormula();
Hans Boehm84614952014-11-25 18:46:17 -0800308 setState(CalculatorState.INIT);
Hans Boehm84614952014-11-25 18:46:17 -0800309 mEvaluator.requireResult();
310 } else {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700311 redisplayAfterFormulaChange();
Hans Boehm84614952014-11-25 18:46:17 -0800312 }
313 // TODO: We're currently not saving and restoring scroll position.
314 // We probably should. Details may require care to deal with:
315 // - new display size
316 // - slow recomputation if we've scrolled far.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700317 }
318
319 @Override
Justin Klaassen9d33cdc2016-02-21 14:16:14 -0800320 protected void onResume() {
321 super.onResume();
Christine Frankseeff27f2016-07-29 12:05:29 -0700322 if (mDisplayView.isToolbarVisible()) {
323 showAndMaybeHideToolbar();
324 }
Justin Klaassen9d33cdc2016-02-21 14:16:14 -0800325 }
326
327 @Override
Justin Klaassenf79d6f62014-08-26 12:27:08 -0700328 protected void onSaveInstanceState(@NonNull Bundle outState) {
Hans Boehm40125442016-01-22 10:35:35 -0800329 mEvaluator.cancelAll(true);
Justin Klaassenf79d6f62014-08-26 12:27:08 -0700330 // If there's an animation in progress, cancel it first to ensure our state is up-to-date.
331 if (mCurrentAnimator != null) {
332 mCurrentAnimator.cancel();
333 }
334
Justin Klaassen4b3af052014-05-27 17:53:10 -0700335 super.onSaveInstanceState(outState);
Hans Boehm84614952014-11-25 18:46:17 -0800336 outState.putInt(KEY_DISPLAY_STATE, mCurrentState.ordinal());
Hans Boehm760a9dc2015-04-20 10:27:12 -0700337 outState.putCharSequence(KEY_UNPROCESSED_CHARS, mUnprocessedChars);
Hans Boehm84614952014-11-25 18:46:17 -0800338 ByteArrayOutputStream byteArrayStream = new ByteArrayOutputStream();
339 try (ObjectOutput out = new ObjectOutputStream(byteArrayStream)) {
340 mEvaluator.saveInstanceState(out);
341 } catch (IOException e) {
342 // Impossible; No IO involved.
343 throw new AssertionError("Impossible IO exception", e);
344 }
345 outState.putByteArray(KEY_EVAL_STATE, byteArrayStream.toByteArray());
Justin Klaassen3e223ea2016-02-05 14:18:06 -0800346 outState.putBoolean(KEY_INVERSE_MODE, mInverseToggle.isSelected());
Christine Frankseeff27f2016-07-29 12:05:29 -0700347 outState.putBoolean(KEY_SHOW_TOOLBAR, mDisplayView.isToolbarVisible());
Justin Klaassen4b3af052014-05-27 17:53:10 -0700348 }
349
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700350 // Set the state, updating delete label and display colors.
351 // This restores display positions on moving to INPUT.
Justin Klaassend48b7562015-04-16 16:51:38 -0700352 // But movement/animation for moving to RESULT has already been done.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700353 private void setState(CalculatorState state) {
354 if (mCurrentState != state) {
Hans Boehm84614952014-11-25 18:46:17 -0800355 if (state == CalculatorState.INPUT) {
356 restoreDisplayPositions();
357 }
Justin Klaassen4b3af052014-05-27 17:53:10 -0700358 mCurrentState = state;
359
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700360 if (mCurrentState == CalculatorState.RESULT) {
361 // No longer do this for ERROR; allow mistakes to be corrected.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700362 mDeleteButton.setVisibility(View.GONE);
363 mClearButton.setVisibility(View.VISIBLE);
364 } else {
365 mDeleteButton.setVisibility(View.VISIBLE);
366 mClearButton.setVisibility(View.GONE);
367 }
368
Annie Chin26e159e2016-05-18 15:17:14 -0700369 if (mOneLine) {
370 if (mCurrentState == CalculatorState.RESULT
371 || mCurrentState == CalculatorState.EVALUATE
372 || mCurrentState == CalculatorState.ANIMATE) {
373 mFormulaText.setVisibility(View.VISIBLE);
374 mResultText.setVisibility(View.VISIBLE);
Annie Chin947d93b2016-06-14 10:18:54 -0700375 } else if (mCurrentState == CalculatorState.ERROR) {
376 mFormulaText.setVisibility(View.INVISIBLE);
377 mResultText.setVisibility(View.VISIBLE);
Annie Chin26e159e2016-05-18 15:17:14 -0700378 } else {
379 mFormulaText.setVisibility(View.VISIBLE);
380 mResultText.setVisibility(View.INVISIBLE);
381 }
382 }
383
Hans Boehm84614952014-11-25 18:46:17 -0800384 if (mCurrentState == CalculatorState.ERROR) {
Chenjie Yu3937b652016-06-01 23:14:26 -0700385 final int errorColor =
386 ContextCompat.getColor(this, R.color.calculator_error_color);
Hans Boehm08e8f322015-04-21 13:18:38 -0700387 mFormulaText.setTextColor(errorColor);
Justin Klaassen44595162015-05-28 17:55:20 -0700388 mResultText.setTextColor(errorColor);
Justin Klaassen8fff1442014-06-19 10:43:29 -0700389 getWindow().setStatusBarColor(errorColor);
Justin Klaassen44595162015-05-28 17:55:20 -0700390 } else if (mCurrentState != CalculatorState.RESULT) {
Chenjie Yu3937b652016-06-01 23:14:26 -0700391 mFormulaText.setTextColor(
392 ContextCompat.getColor(this, R.color.display_formula_text_color));
393 mResultText.setTextColor(
394 ContextCompat.getColor(this, R.color.display_result_text_color));
395 getWindow().setStatusBarColor(
396 ContextCompat.getColor(this, R.color.calculator_accent_color));
Justin Klaassen4b3af052014-05-27 17:53:10 -0700397 }
Justin Klaassend48b7562015-04-16 16:51:38 -0700398
399 invalidateOptionsMenu();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700400 }
401 }
402
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700403 @Override
404 public void onActionModeStarted(ActionMode mode) {
405 super.onActionModeStarted(mode);
406 if (mode.getTag() == CalculatorText.TAG_ACTION_MODE) {
407 mFormulaContainer.scrollTo(mFormulaText.getRight(), 0);
408 }
409 }
410
Chenjie Yu3937b652016-06-01 23:14:26 -0700411 /**
412 * Stop any active ActionMode or ContextMenu for copy/paste actions.
413 * Return true if there was one.
414 */
415 private boolean stopActionModeOrContextMenu() {
416 if (mResultText.stopActionModeOrContextMenu()) {
Hans Boehm1176f232015-05-11 16:26:03 -0700417 return true;
418 }
Chenjie Yu3937b652016-06-01 23:14:26 -0700419 if (mFormulaText.stopActionModeOrContextMenu()) {
Hans Boehm1176f232015-05-11 16:26:03 -0700420 return true;
421 }
422 return false;
423 }
424
Justin Klaassen4b3af052014-05-27 17:53:10 -0700425 @Override
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700426 public void onUserInteraction() {
427 super.onUserInteraction();
428
429 // If there's an animation in progress, end it immediately, so the user interaction can
430 // be handled.
431 if (mCurrentAnimator != null) {
432 mCurrentAnimator.end();
433 }
434 }
435
436 @Override
Justin Klaassen3b4d13d2014-06-06 18:18:37 +0100437 public void onBackPressed() {
Chenjie Yu3937b652016-06-01 23:14:26 -0700438 if (!stopActionModeOrContextMenu()) {
Hans Boehm1176f232015-05-11 16:26:03 -0700439 if (mPadViewPager != null && mPadViewPager.getCurrentItem() != 0) {
440 // Select the previous pad.
441 mPadViewPager.setCurrentItem(mPadViewPager.getCurrentItem() - 1);
442 } else {
443 // If the user is currently looking at the first pad (or the pad is not paged),
444 // allow the system to handle the Back button.
445 super.onBackPressed();
446 }
Justin Klaassen3b4d13d2014-06-06 18:18:37 +0100447 }
448 }
449
450 @Override
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700451 public boolean onKeyUp(int keyCode, KeyEvent event) {
Justin Klaassen83959da2016-04-06 11:55:24 -0700452 // Allow the system to handle special key codes (e.g. "BACK" or "DPAD").
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700453 switch (keyCode) {
Justin Klaassen83959da2016-04-06 11:55:24 -0700454 case KeyEvent.KEYCODE_BACK:
Christine Franksf9ba2202016-10-20 17:20:19 -0700455 case KeyEvent.KEYCODE_ESCAPE:
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700456 case KeyEvent.KEYCODE_DPAD_UP:
457 case KeyEvent.KEYCODE_DPAD_DOWN:
458 case KeyEvent.KEYCODE_DPAD_LEFT:
459 case KeyEvent.KEYCODE_DPAD_RIGHT:
460 return super.onKeyUp(keyCode, event);
461 }
462
Chenjie Yu3937b652016-06-01 23:14:26 -0700463 // Stop the action mode or context menu if it's showing.
464 stopActionModeOrContextMenu();
Justin Klaassend12e0622016-04-27 16:26:47 -0700465
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700466 // Always cancel unrequested in-progress evaluation, so that we don't have to worry about
467 // subsequent asynchronous completion.
468 // Requested in-progress evaluations are handled below.
469 if (mCurrentState != CalculatorState.EVALUATE) {
470 mEvaluator.cancelAll(true);
471 }
472
473 switch (keyCode) {
474 case KeyEvent.KEYCODE_NUMPAD_ENTER:
475 case KeyEvent.KEYCODE_ENTER:
476 case KeyEvent.KEYCODE_DPAD_CENTER:
477 mCurrentButton = mEqualButton;
478 onEquals();
479 return true;
480 case KeyEvent.KEYCODE_DEL:
481 mCurrentButton = mDeleteButton;
482 onDelete();
483 return true;
Annie Chin56bcbf12016-09-23 17:04:22 -0700484 case KeyEvent.KEYCODE_CLEAR:
485 mCurrentButton = mClearButton;
486 onClear();
487 return true;
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700488 default:
489 cancelIfEvaluating(false);
490 final int raw = event.getKeyCharacterMap().get(keyCode, event.getMetaState());
491 if ((raw & KeyCharacterMap.COMBINING_ACCENT) != 0) {
492 return true; // discard
493 }
494 // Try to discard non-printing characters and the like.
495 // The user will have to explicitly delete other junk that gets past us.
496 if (Character.isIdentifierIgnorable(raw) || Character.isWhitespace(raw)) {
497 return true;
498 }
499 char c = (char) raw;
500 if (c == '=') {
501 mCurrentButton = mEqualButton;
502 onEquals();
503 } else {
504 addChars(String.valueOf(c), true);
505 redisplayAfterFormulaChange();
506 }
507 return true;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700508 }
509 }
510
Justin Klaassene2711cb2015-05-28 11:13:17 -0700511 /**
512 * Invoked whenever the inverse button is toggled to update the UI.
513 *
514 * @param showInverse {@code true} if inverse functions should be shown
515 */
516 private void onInverseToggled(boolean showInverse) {
Justin Klaassen3e223ea2016-02-05 14:18:06 -0800517 mInverseToggle.setSelected(showInverse);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700518 if (showInverse) {
519 mInverseToggle.setContentDescription(getString(R.string.desc_inv_on));
Justin Klaassen721ec842015-05-28 14:30:08 -0700520 for (View invertibleButton : mInvertibleButtons) {
521 invertibleButton.setVisibility(View.GONE);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700522 }
523 for (View inverseButton : mInverseButtons) {
524 inverseButton.setVisibility(View.VISIBLE);
525 }
526 } else {
527 mInverseToggle.setContentDescription(getString(R.string.desc_inv_off));
Justin Klaassen721ec842015-05-28 14:30:08 -0700528 for (View invertibleButton : mInvertibleButtons) {
529 invertibleButton.setVisibility(View.VISIBLE);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700530 }
531 for (View inverseButton : mInverseButtons) {
532 inverseButton.setVisibility(View.GONE);
533 }
534 }
535 }
536
537 /**
Christine Frankseeff27f2016-07-29 12:05:29 -0700538 * Invoked whenever the deg/rad mode may have changed to update the UI. Note that the mode has
539 * not necessarily actually changed where this is invoked.
Justin Klaassene2711cb2015-05-28 11:13:17 -0700540 *
541 * @param degreeMode {@code true} if in degree mode
542 */
543 private void onModeChanged(boolean degreeMode) {
544 if (degreeMode) {
Justin Klaassend48b7562015-04-16 16:51:38 -0700545 mModeView.setText(R.string.mode_deg);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700546 mModeView.setContentDescription(getString(R.string.desc_mode_deg));
547
548 mModeToggle.setText(R.string.mode_rad);
549 mModeToggle.setContentDescription(getString(R.string.desc_switch_rad));
Hans Boehmbfe8c222015-04-02 16:26:07 -0700550 } else {
Justin Klaassend48b7562015-04-16 16:51:38 -0700551 mModeView.setText(R.string.mode_rad);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700552 mModeView.setContentDescription(getString(R.string.desc_mode_rad));
553
554 mModeToggle.setText(R.string.mode_deg);
555 mModeToggle.setContentDescription(getString(R.string.desc_switch_deg));
Hans Boehmbfe8c222015-04-02 16:26:07 -0700556 }
557 }
Hans Boehm84614952014-11-25 18:46:17 -0800558
Hans Boehm5d79d102015-09-16 16:33:47 -0700559 /**
560 * Switch to INPUT from RESULT state in response to input of the specified button_id.
561 * View.NO_ID is treated as an incomplete function id.
562 */
563 private void switchToInput(int button_id) {
564 if (KeyMaps.isBinary(button_id) || KeyMaps.isSuffix(button_id)) {
565 mEvaluator.collapse();
566 } else {
567 announceClearedForAccessibility();
568 mEvaluator.clear();
569 }
570 setState(CalculatorState.INPUT);
571 }
572
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700573 // Add the given button id to input expression.
574 // If appropriate, clear the expression before doing so.
575 private void addKeyToExpr(int id) {
576 if (mCurrentState == CalculatorState.ERROR) {
577 setState(CalculatorState.INPUT);
578 } else if (mCurrentState == CalculatorState.RESULT) {
Hans Boehm5d79d102015-09-16 16:33:47 -0700579 switchToInput(id);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700580 }
581 if (!mEvaluator.append(id)) {
582 // TODO: Some user visible feedback?
583 }
584 }
585
Hans Boehm017de982015-06-10 17:46:03 -0700586 /**
587 * Add the given button id to input expression, assuming it was explicitly
588 * typed/touched.
589 * We perform slightly more aggressive correction than in pasted expressions.
590 */
591 private void addExplicitKeyToExpr(int id) {
592 if (mCurrentState == CalculatorState.INPUT && id == R.id.op_sub) {
593 mEvaluator.getExpr().removeTrailingAdditiveOperators();
594 }
595 addKeyToExpr(id);
596 }
597
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700598 private void redisplayAfterFormulaChange() {
599 // TODO: Could do this more incrementally.
600 redisplayFormula();
601 setState(CalculatorState.INPUT);
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800602 if (haveUnprocessed()) {
Justin Klaassen44595162015-05-28 17:55:20 -0700603 mResultText.clear();
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800604 // Force reevaluation when text is deleted, even if expression is unchanged.
605 mEvaluator.touch();
606 } else {
607 if (mEvaluator.getExpr().hasInterestingOps()) {
608 mEvaluator.evaluateAndShowResult();
609 } else {
610 mResultText.clear();
611 }
Hans Boehmc023b732015-04-29 11:30:47 -0700612 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700613 }
614
Hans Boehm52d477a2016-04-01 17:42:50 -0700615 /**
616 * Show the toolbar.
617 * Automatically hide it again if it's not relevant to current formula.
618 */
619 private void showAndMaybeHideToolbar() {
620 final boolean shouldBeVisible =
621 mCurrentState == CalculatorState.INPUT && mEvaluator.hasTrigFuncs();
622 mDisplayView.showToolbar(!shouldBeVisible);
623 }
624
625 /**
626 * Display or hide the toolbar depending on calculator state.
627 */
628 private void showOrHideToolbar() {
629 final boolean shouldBeVisible =
630 mCurrentState == CalculatorState.INPUT && mEvaluator.hasTrigFuncs();
631 if (shouldBeVisible) {
632 mDisplayView.showToolbar(false);
633 } else {
634 mDisplayView.hideToolbar();
635 }
636 }
637
Justin Klaassen4b3af052014-05-27 17:53:10 -0700638 public void onButtonClick(View view) {
Hans Boehmc1ea0912015-06-19 15:05:07 -0700639 // Any animation is ended before we get here.
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700640 mCurrentButton = view;
Chenjie Yu3937b652016-06-01 23:14:26 -0700641 stopActionModeOrContextMenu();
Justin Klaassen9d33cdc2016-02-21 14:16:14 -0800642
Hans Boehmc1ea0912015-06-19 15:05:07 -0700643 // See onKey above for the rationale behind some of the behavior below:
644 if (mCurrentState != CalculatorState.EVALUATE) {
645 // Cancel evaluations that were not specifically requested.
646 mEvaluator.cancelAll(true);
Hans Boehm84614952014-11-25 18:46:17 -0800647 }
Justin Klaassen9d33cdc2016-02-21 14:16:14 -0800648
Justin Klaassend48b7562015-04-16 16:51:38 -0700649 final int id = view.getId();
Hans Boehm84614952014-11-25 18:46:17 -0800650 switch (id) {
Justin Klaassen4b3af052014-05-27 17:53:10 -0700651 case R.id.eq:
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700652 onEquals();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700653 break;
654 case R.id.del:
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700655 onDelete();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700656 break;
657 case R.id.clr:
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700658 onClear();
Hans Boehm52d477a2016-04-01 17:42:50 -0700659 return; // Toolbar visibility adjusted at end of animation.
Justin Klaassene2711cb2015-05-28 11:13:17 -0700660 case R.id.toggle_inv:
661 final boolean selected = !mInverseToggle.isSelected();
662 mInverseToggle.setSelected(selected);
663 onInverseToggled(selected);
Hans Boehmc1ea0912015-06-19 15:05:07 -0700664 if (mCurrentState == CalculatorState.RESULT) {
665 mResultText.redisplay(); // In case we cancelled reevaluation.
666 }
Justin Klaassene2711cb2015-05-28 11:13:17 -0700667 break;
668 case R.id.toggle_mode:
Hans Boehmc1ea0912015-06-19 15:05:07 -0700669 cancelIfEvaluating(false);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700670 final boolean mode = !mEvaluator.getDegreeMode();
Hans Boehmbfe8c222015-04-02 16:26:07 -0700671 if (mCurrentState == CalculatorState.RESULT) {
672 mEvaluator.collapse(); // Capture result evaluated in old mode
673 redisplayFormula();
674 }
675 // In input mode, we reinterpret already entered trig functions.
676 mEvaluator.setDegreeMode(mode);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700677 onModeChanged(mode);
Christine Frankseeff27f2016-07-29 12:05:29 -0700678 // Show the toolbar to highlight the mode change.
679 showAndMaybeHideToolbar();
Hans Boehmbfe8c222015-04-02 16:26:07 -0700680 setState(CalculatorState.INPUT);
Justin Klaassen44595162015-05-28 17:55:20 -0700681 mResultText.clear();
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800682 if (!haveUnprocessed() && mEvaluator.getExpr().hasInterestingOps()) {
Hans Boehmc023b732015-04-29 11:30:47 -0700683 mEvaluator.evaluateAndShowResult();
684 }
Christine Frankseeff27f2016-07-29 12:05:29 -0700685 return;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700686 default:
Hans Boehmc1ea0912015-06-19 15:05:07 -0700687 cancelIfEvaluating(false);
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800688 if (haveUnprocessed()) {
689 // For consistency, append as uninterpreted characters.
690 // This may actually be useful for a left parenthesis.
691 addChars(KeyMaps.toString(this, id), true);
692 } else {
693 addExplicitKeyToExpr(id);
694 redisplayAfterFormulaChange();
695 }
Justin Klaassen4b3af052014-05-27 17:53:10 -0700696 break;
697 }
Hans Boehm52d477a2016-04-01 17:42:50 -0700698 showOrHideToolbar();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700699 }
700
Hans Boehm84614952014-11-25 18:46:17 -0800701 void redisplayFormula() {
Hans Boehm8a4f81c2015-07-09 10:41:25 -0700702 SpannableStringBuilder formula = mEvaluator.getExpr().toSpannableStringBuilder(this);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700703 if (mUnprocessedChars != null) {
704 // Add and highlight characters we couldn't process.
Hans Boehm8a4f81c2015-07-09 10:41:25 -0700705 formula.append(mUnprocessedChars, mUnprocessedColorSpan,
706 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700707 }
Hans Boehm8a4f81c2015-07-09 10:41:25 -0700708 mFormulaText.changeTextTo(formula);
Annie Chinf360ef02016-03-10 13:45:39 -0800709 mFormulaText.setContentDescription(TextUtils.isEmpty(formula)
Justin Klaassend1831412016-07-19 21:59:10 -0700710 ? getString(R.string.desc_formula) : null);
Hans Boehm84614952014-11-25 18:46:17 -0800711 }
712
Justin Klaassen4b3af052014-05-27 17:53:10 -0700713 @Override
714 public boolean onLongClick(View view) {
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700715 mCurrentButton = view;
716
Justin Klaassen4b3af052014-05-27 17:53:10 -0700717 if (view.getId() == R.id.del) {
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700718 onClear();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700719 return true;
720 }
721 return false;
722 }
723
Hans Boehm84614952014-11-25 18:46:17 -0800724 // Initial evaluation completed successfully. Initiate display.
Hans Boehma0e45f32015-05-30 13:20:35 -0700725 public void onEvaluate(int initDisplayPrec, int msd, int leastDigPos,
726 String truncatedWholeNumber) {
Justin Klaassend48b7562015-04-16 16:51:38 -0700727 // Invalidate any options that may depend on the current result.
728 invalidateOptionsMenu();
729
Hans Boehma0e45f32015-05-30 13:20:35 -0700730 mResultText.displayResult(initDisplayPrec, msd, leastDigPos, truncatedWholeNumber);
Hans Boehm61568a12015-05-18 18:25:41 -0700731 if (mCurrentState != CalculatorState.INPUT) { // in EVALUATE or INIT state
Hans Boehm84614952014-11-25 18:46:17 -0800732 onResult(mCurrentState != CalculatorState.INIT);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700733 }
Hans Boehm84614952014-11-25 18:46:17 -0800734 }
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700735
Hans Boehmc1ea0912015-06-19 15:05:07 -0700736 // Reset state to reflect evaluator cancellation. Invoked by evaluator.
Hans Boehm84614952014-11-25 18:46:17 -0800737 public void onCancelled() {
738 // We should be in EVALUATE state.
Hans Boehm84614952014-11-25 18:46:17 -0800739 setState(CalculatorState.INPUT);
Justin Klaassen44595162015-05-28 17:55:20 -0700740 mResultText.clear();
Hans Boehm84614952014-11-25 18:46:17 -0800741 }
742
743 // Reevaluation completed; ask result to redisplay current value.
744 public void onReevaluate()
745 {
Justin Klaassen44595162015-05-28 17:55:20 -0700746 mResultText.redisplay();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700747 }
748
Justin Klaassenfed941a2014-06-09 18:42:40 +0100749 @Override
750 public void onTextSizeChanged(final TextView textView, float oldSize) {
751 if (mCurrentState != CalculatorState.INPUT) {
752 // Only animate text changes that occur from user input.
753 return;
754 }
755
756 // Calculate the values needed to perform the scale and translation animations,
757 // maintaining the same apparent baseline for the displayed text.
758 final float textScale = oldSize / textView.getTextSize();
759 final float translationX = (1.0f - textScale) *
760 (textView.getWidth() / 2.0f - textView.getPaddingEnd());
761 final float translationY = (1.0f - textScale) *
762 (textView.getHeight() / 2.0f - textView.getPaddingBottom());
763
764 final AnimatorSet animatorSet = new AnimatorSet();
765 animatorSet.playTogether(
766 ObjectAnimator.ofFloat(textView, View.SCALE_X, textScale, 1.0f),
767 ObjectAnimator.ofFloat(textView, View.SCALE_Y, textScale, 1.0f),
768 ObjectAnimator.ofFloat(textView, View.TRANSLATION_X, translationX, 0.0f),
769 ObjectAnimator.ofFloat(textView, View.TRANSLATION_Y, translationY, 0.0f));
Justin Klaassen94db7202014-06-11 11:22:31 -0700770 animatorSet.setDuration(getResources().getInteger(android.R.integer.config_mediumAnimTime));
Justin Klaassenfed941a2014-06-09 18:42:40 +0100771 animatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
772 animatorSet.start();
773 }
774
Hans Boehmc1ea0912015-06-19 15:05:07 -0700775 /**
776 * Cancel any in-progress explicitly requested evaluations.
777 * @param quiet suppress pop-up message. Explicit evaluation can change the expression
778 value, and certainly changes the display, so it seems reasonable to warn.
779 * @return true if there was such an evaluation
780 */
781 private boolean cancelIfEvaluating(boolean quiet) {
782 if (mCurrentState == CalculatorState.EVALUATE) {
783 mEvaluator.cancelAll(quiet);
784 return true;
785 } else {
786 return false;
787 }
788 }
789
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800790 private boolean haveUnprocessed() {
791 return mUnprocessedChars != null && !mUnprocessedChars.isEmpty();
792 }
793
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700794 private void onEquals() {
Hans Boehm56d6e762016-06-06 11:46:29 -0700795 // Ignore if in non-INPUT state, or if there are no operators.
Justin Klaassena8075af2016-07-27 15:24:45 -0700796 if (mCurrentState == CalculatorState.INPUT) {
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800797 if (haveUnprocessed()) {
Justin Klaassena8075af2016-07-27 15:24:45 -0700798 setState(CalculatorState.EVALUATE);
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800799 onError(R.string.error_syntax);
Justin Klaassena8075af2016-07-27 15:24:45 -0700800 } else if (mEvaluator.getExpr().hasInterestingOps()) {
801 setState(CalculatorState.EVALUATE);
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800802 mEvaluator.requireResult();
803 }
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700804 }
805 }
806
807 private void onDelete() {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700808 // Delete works like backspace; remove the last character or operator from the expression.
809 // Note that we handle keyboard delete exactly like the delete button. For
810 // example the delete button can be used to delete a character from an incomplete
811 // function name typed on a physical keyboard.
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700812 // This should be impossible in RESULT state.
Hans Boehmc1ea0912015-06-19 15:05:07 -0700813 // If there is an in-progress explicit evaluation, just cancel it and return.
814 if (cancelIfEvaluating(false)) return;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700815 setState(CalculatorState.INPUT);
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800816 if (haveUnprocessed()) {
817 mUnprocessedChars = mUnprocessedChars.substring(0, mUnprocessedChars.length() - 1);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700818 } else {
Hans Boehmc023b732015-04-29 11:30:47 -0700819 mEvaluator.delete();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700820 }
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800821 if (mEvaluator.getExpr().isEmpty() && !haveUnprocessed()) {
Hans Boehmdb6f9992015-08-19 12:32:56 -0700822 // Resulting formula won't be announced, since it's empty.
823 announceClearedForAccessibility();
824 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700825 redisplayAfterFormulaChange();
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700826 }
827
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700828 private void reveal(View sourceView, int colorRes, AnimatorListener listener) {
Justin Klaassen06360f92014-08-28 11:08:44 -0700829 final ViewGroupOverlay groupOverlay =
830 (ViewGroupOverlay) getWindow().getDecorView().getOverlay();
Justin Klaassen8fff1442014-06-19 10:43:29 -0700831
832 final Rect displayRect = new Rect();
Justin Klaassen06360f92014-08-28 11:08:44 -0700833 mDisplayView.getGlobalVisibleRect(displayRect);
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700834
835 // Make reveal cover the display and status bar.
836 final View revealView = new View(this);
Justin Klaassen8fff1442014-06-19 10:43:29 -0700837 revealView.setBottom(displayRect.bottom);
838 revealView.setLeft(displayRect.left);
839 revealView.setRight(displayRect.right);
Chenjie Yu3937b652016-06-01 23:14:26 -0700840 revealView.setBackgroundColor(ContextCompat.getColor(this, colorRes));
Justin Klaassen06360f92014-08-28 11:08:44 -0700841 groupOverlay.add(revealView);
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700842
Justin Klaassen4b3af052014-05-27 17:53:10 -0700843 final int[] clearLocation = new int[2];
844 sourceView.getLocationInWindow(clearLocation);
845 clearLocation[0] += sourceView.getWidth() / 2;
846 clearLocation[1] += sourceView.getHeight() / 2;
847
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700848 final int revealCenterX = clearLocation[0] - revealView.getLeft();
849 final int revealCenterY = clearLocation[1] - revealView.getTop();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700850
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700851 final double x1_2 = Math.pow(revealView.getLeft() - revealCenterX, 2);
852 final double x2_2 = Math.pow(revealView.getRight() - revealCenterX, 2);
853 final double y_2 = Math.pow(revealView.getTop() - revealCenterY, 2);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700854 final float revealRadius = (float) Math.max(Math.sqrt(x1_2 + y_2), Math.sqrt(x2_2 + y_2));
855
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700856 final Animator revealAnimator =
857 ViewAnimationUtils.createCircularReveal(revealView,
ztenghui3d6ecaf2014-06-05 09:56:00 -0700858 revealCenterX, revealCenterY, 0.0f, revealRadius);
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700859 revealAnimator.setDuration(
Justin Klaassen4b3af052014-05-27 17:53:10 -0700860 getResources().getInteger(android.R.integer.config_longAnimTime));
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700861 revealAnimator.addListener(listener);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700862
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700863 final Animator alphaAnimator = ObjectAnimator.ofFloat(revealView, View.ALPHA, 0.0f);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700864 alphaAnimator.setDuration(
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700865 getResources().getInteger(android.R.integer.config_mediumAnimTime));
Justin Klaassen4b3af052014-05-27 17:53:10 -0700866
867 final AnimatorSet animatorSet = new AnimatorSet();
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700868 animatorSet.play(revealAnimator).before(alphaAnimator);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700869 animatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
870 animatorSet.addListener(new AnimatorListenerAdapter() {
871 @Override
Justin Klaassen4b3af052014-05-27 17:53:10 -0700872 public void onAnimationEnd(Animator animator) {
Justin Klaassen8fff1442014-06-19 10:43:29 -0700873 groupOverlay.remove(revealView);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700874 mCurrentAnimator = null;
875 }
876 });
877
878 mCurrentAnimator = animatorSet;
879 animatorSet.start();
880 }
881
Hans Boehmdb6f9992015-08-19 12:32:56 -0700882 private void announceClearedForAccessibility() {
883 mResultText.announceForAccessibility(getResources().getString(R.string.cleared));
Hans Boehmccc55662015-07-07 14:16:59 -0700884 }
885
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700886 private void onClear() {
Justin Klaassen1a428cf2016-02-24 15:58:18 -0800887 if (mEvaluator.getExpr().isEmpty() && !haveUnprocessed()) {
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700888 return;
889 }
Hans Boehmc1ea0912015-06-19 15:05:07 -0700890 cancelIfEvaluating(true);
Hans Boehmdb6f9992015-08-19 12:32:56 -0700891 announceClearedForAccessibility();
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700892 reveal(mCurrentButton, R.color.calculator_accent_color, new AnimatorListenerAdapter() {
893 @Override
894 public void onAnimationEnd(Animator animation) {
Hans Boehm760a9dc2015-04-20 10:27:12 -0700895 mUnprocessedChars = null;
Justin Klaassen44595162015-05-28 17:55:20 -0700896 mResultText.clear();
Hans Boehm760a9dc2015-04-20 10:27:12 -0700897 mEvaluator.clear();
898 setState(CalculatorState.INPUT);
Hans Boehm52d477a2016-04-01 17:42:50 -0700899 showOrHideToolbar();
Hans Boehm84614952014-11-25 18:46:17 -0800900 redisplayFormula();
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700901 }
902 });
903 }
904
Hans Boehm84614952014-11-25 18:46:17 -0800905 // Evaluation encountered en error. Display the error.
906 void onError(final int errorResourceId) {
Hans Boehmfbcef702015-04-27 18:07:47 -0700907 if (mCurrentState == CalculatorState.EVALUATE) {
908 setState(CalculatorState.ANIMATE);
Hans Boehmccc55662015-07-07 14:16:59 -0700909 mResultText.announceForAccessibility(getResources().getString(errorResourceId));
Hans Boehmfbcef702015-04-27 18:07:47 -0700910 reveal(mCurrentButton, R.color.calculator_error_color,
911 new AnimatorListenerAdapter() {
912 @Override
913 public void onAnimationEnd(Animator animation) {
914 setState(CalculatorState.ERROR);
Justin Klaassen44595162015-05-28 17:55:20 -0700915 mResultText.displayError(errorResourceId);
Hans Boehmfbcef702015-04-27 18:07:47 -0700916 }
917 });
918 } else if (mCurrentState == CalculatorState.INIT) {
919 setState(CalculatorState.ERROR);
Justin Klaassen44595162015-05-28 17:55:20 -0700920 mResultText.displayError(errorResourceId);
Hans Boehmc023b732015-04-29 11:30:47 -0700921 } else {
Justin Klaassen44595162015-05-28 17:55:20 -0700922 mResultText.clear();
Justin Klaassen2be4fdb2014-08-06 19:54:09 -0700923 }
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700924 }
925
Hans Boehm84614952014-11-25 18:46:17 -0800926 // Animate movement of result into the top formula slot.
927 // Result window now remains translated in the top slot while the result is displayed.
928 // (We convert it back to formula use only when the user provides new input.)
Justin Klaassen44595162015-05-28 17:55:20 -0700929 // Historical note: In the Lollipop version, this invisibly and instantaneously moved
Hans Boehm84614952014-11-25 18:46:17 -0800930 // formula and result displays back at the end of the animation. We no longer do that,
931 // so that we can continue to properly support scrolling of the result.
932 // We assume the result already contains the text to be expanded.
933 private void onResult(boolean animate) {
Justin Klaassen44595162015-05-28 17:55:20 -0700934 // Calculate the textSize that would be used to display the result in the formula.
935 // For scrollable results just use the minimum textSize to maximize the number of digits
936 // that are visible on screen.
937 float textSize = mFormulaText.getMinimumTextSize();
938 if (!mResultText.isScrollable()) {
939 textSize = mFormulaText.getVariableTextSize(mResultText.getText().toString());
940 }
941
942 // Scale the result to match the calculated textSize, minimizing the jump-cut transition
943 // when a result is reused in a subsequent expression.
944 final float resultScale = textSize / mResultText.getTextSize();
945
946 // Set the result's pivot to match its gravity.
947 mResultText.setPivotX(mResultText.getWidth() - mResultText.getPaddingRight());
948 mResultText.setPivotY(mResultText.getHeight() - mResultText.getPaddingBottom());
949
950 // Calculate the necessary translations so the result takes the place of the formula and
951 // the formula moves off the top of the screen.
Annie Chin28589dc2016-06-09 17:50:51 -0700952 final float resultTranslationY = (mFormulaContainer.getBottom() - mResultText.getBottom())
953 - (mFormulaText.getPaddingBottom() - mResultText.getPaddingBottom());
954 float formulaTranslationY = -mFormulaContainer.getBottom();
Annie Chin26e159e2016-05-18 15:17:14 -0700955 if (mOneLine) {
956 // Position the result text.
957 mResultText.setY(mResultText.getBottom());
Annie Chin28589dc2016-06-09 17:50:51 -0700958 formulaTranslationY = -(findViewById(R.id.toolbar).getBottom()
959 + mFormulaContainer.getBottom());
Annie Chin26e159e2016-05-18 15:17:14 -0700960 }
Justin Klaassen4b3af052014-05-27 17:53:10 -0700961
Justin Klaassen44595162015-05-28 17:55:20 -0700962 // Change the result's textColor to match the formula.
963 final int formulaTextColor = mFormulaText.getCurrentTextColor();
964
Hans Boehm84614952014-11-25 18:46:17 -0800965 if (animate) {
Hans Boehmccc55662015-07-07 14:16:59 -0700966 mResultText.announceForAccessibility(getResources().getString(R.string.desc_eq));
967 mResultText.announceForAccessibility(mResultText.getText());
Hans Boehmc1ea0912015-06-19 15:05:07 -0700968 setState(CalculatorState.ANIMATE);
Hans Boehm84614952014-11-25 18:46:17 -0800969 final AnimatorSet animatorSet = new AnimatorSet();
970 animatorSet.playTogether(
Justin Klaassen44595162015-05-28 17:55:20 -0700971 ObjectAnimator.ofPropertyValuesHolder(mResultText,
972 PropertyValuesHolder.ofFloat(View.SCALE_X, resultScale),
973 PropertyValuesHolder.ofFloat(View.SCALE_Y, resultScale),
974 PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, resultTranslationY)),
975 ObjectAnimator.ofArgb(mResultText, TEXT_COLOR, formulaTextColor),
Annie Chine918fd22016-03-09 11:07:54 -0800976 ObjectAnimator.ofFloat(mFormulaContainer, View.TRANSLATION_Y,
977 formulaTranslationY));
Justin Klaassen44595162015-05-28 17:55:20 -0700978 animatorSet.setDuration(getResources().getInteger(
979 android.R.integer.config_longAnimTime));
Hans Boehm84614952014-11-25 18:46:17 -0800980 animatorSet.addListener(new AnimatorListenerAdapter() {
981 @Override
Hans Boehm84614952014-11-25 18:46:17 -0800982 public void onAnimationEnd(Animator animation) {
983 setState(CalculatorState.RESULT);
984 mCurrentAnimator = null;
985 }
986 });
Justin Klaassen4b3af052014-05-27 17:53:10 -0700987
Hans Boehm84614952014-11-25 18:46:17 -0800988 mCurrentAnimator = animatorSet;
989 animatorSet.start();
990 } else /* No animation desired; get there fast, e.g. when restarting */ {
Justin Klaassen44595162015-05-28 17:55:20 -0700991 mResultText.setScaleX(resultScale);
992 mResultText.setScaleY(resultScale);
993 mResultText.setTranslationY(resultTranslationY);
994 mResultText.setTextColor(formulaTextColor);
Annie Chine918fd22016-03-09 11:07:54 -0800995 mFormulaContainer.setTranslationY(formulaTranslationY);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700996 setState(CalculatorState.RESULT);
Hans Boehm84614952014-11-25 18:46:17 -0800997 }
Justin Klaassen4b3af052014-05-27 17:53:10 -0700998 }
Hans Boehm84614952014-11-25 18:46:17 -0800999
1000 // Restore positions of the formula and result displays back to their original,
1001 // pre-animation state.
1002 private void restoreDisplayPositions() {
1003 // Clear result.
Justin Klaassen44595162015-05-28 17:55:20 -07001004 mResultText.setText("");
Hans Boehm84614952014-11-25 18:46:17 -08001005 // Reset all of the values modified during the animation.
Justin Klaassen44595162015-05-28 17:55:20 -07001006 mResultText.setScaleX(1.0f);
1007 mResultText.setScaleY(1.0f);
1008 mResultText.setTranslationX(0.0f);
1009 mResultText.setTranslationY(0.0f);
Annie Chine918fd22016-03-09 11:07:54 -08001010 mFormulaContainer.setTranslationY(0.0f);
Hans Boehm84614952014-11-25 18:46:17 -08001011
Hans Boehm08e8f322015-04-21 13:18:38 -07001012 mFormulaText.requestFocus();
Hans Boehm5e6a0ca2015-09-22 17:09:01 -07001013 }
1014
1015 @Override
1016 public void onClick(AlertDialogFragment fragment, int which) {
1017 if (which == DialogInterface.BUTTON_POSITIVE) {
1018 // Timeout extension request.
1019 mEvaluator.setLongTimeOut();
1020 }
1021 }
Hans Boehm84614952014-11-25 18:46:17 -08001022
Justin Klaassend48b7562015-04-16 16:51:38 -07001023 @Override
1024 public boolean onCreateOptionsMenu(Menu menu) {
Justin Klaassend36d63e2015-05-05 12:59:36 -07001025 super.onCreateOptionsMenu(menu);
1026
1027 getMenuInflater().inflate(R.menu.activity_calculator, menu);
Justin Klaassend48b7562015-04-16 16:51:38 -07001028 return true;
1029 }
1030
1031 @Override
1032 public boolean onPrepareOptionsMenu(Menu menu) {
Justin Klaassend36d63e2015-05-05 12:59:36 -07001033 super.onPrepareOptionsMenu(menu);
1034
1035 // Show the leading option when displaying a result.
1036 menu.findItem(R.id.menu_leading).setVisible(mCurrentState == CalculatorState.RESULT);
1037
1038 // Show the fraction option when displaying a rational result.
1039 menu.findItem(R.id.menu_fraction).setVisible(mCurrentState == CalculatorState.RESULT
Hans Boehm995e5eb2016-02-08 11:03:01 -08001040 && mEvaluator.getResult().exactlyDisplayable());
Justin Klaassend36d63e2015-05-05 12:59:36 -07001041
Justin Klaassend48b7562015-04-16 16:51:38 -07001042 return true;
Hans Boehm84614952014-11-25 18:46:17 -08001043 }
1044
1045 @Override
Justin Klaassend48b7562015-04-16 16:51:38 -07001046 public boolean onOptionsItemSelected(MenuItem item) {
Hans Boehm84614952014-11-25 18:46:17 -08001047 switch (item.getItemId()) {
Annie Chinabd202f2016-10-14 14:23:45 -07001048 case R.id.menu_history:
1049 getFragmentManager().beginTransaction()
1050 .replace(android.R.id.content, new HistoryFragment(), HistoryFragment.TAG)
1051 .addToBackStack(HistoryFragment.TAG)
1052 .commit();
1053
1054 return true;
Justin Klaassend36d63e2015-05-05 12:59:36 -07001055 case R.id.menu_leading:
1056 displayFull();
Hans Boehm84614952014-11-25 18:46:17 -08001057 return true;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001058 case R.id.menu_fraction:
1059 displayFraction();
1060 return true;
Justin Klaassend36d63e2015-05-05 12:59:36 -07001061 case R.id.menu_licenses:
1062 startActivity(new Intent(this, Licenses.class));
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001063 return true;
Hans Boehm84614952014-11-25 18:46:17 -08001064 default:
1065 return super.onOptionsItemSelected(item);
1066 }
1067 }
1068
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001069 private void displayMessage(String s) {
Hans Boehm5e6a0ca2015-09-22 17:09:01 -07001070 AlertDialogFragment.showMessageDialog(this, s, null);
Hans Boehm84614952014-11-25 18:46:17 -08001071 }
1072
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001073 private void displayFraction() {
Hans Boehm995e5eb2016-02-08 11:03:01 -08001074 UnifiedReal result = mEvaluator.getResult();
Hans Boehm013969e2015-04-13 20:29:47 -07001075 displayMessage(KeyMaps.translateResult(result.toNiceString()));
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001076 }
1077
1078 // Display full result to currently evaluated precision
1079 private void displayFull() {
1080 Resources res = getResources();
Hans Boehm24c91ed2016-06-30 18:53:44 -07001081 String msg = mResultText.getFullText(true /* withSeparators */) + " ";
Justin Klaassen44595162015-05-28 17:55:20 -07001082 if (mResultText.fullTextIsExact()) {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001083 msg += res.getString(R.string.exact);
1084 } else {
1085 msg += res.getString(R.string.approximate);
1086 }
1087 displayMessage(msg);
1088 }
1089
Hans Boehm017de982015-06-10 17:46:03 -07001090 /**
1091 * Add input characters to the end of the expression.
1092 * Map them to the appropriate button pushes when possible. Leftover characters
1093 * are added to mUnprocessedChars, which is presumed to immediately precede the newly
1094 * added characters.
Hans Boehm65a99a42016-02-03 18:16:07 -08001095 * @param moreChars characters to be added
1096 * @param explicit these characters were explicitly typed by the user, not pasted
Hans Boehm017de982015-06-10 17:46:03 -07001097 */
1098 private void addChars(String moreChars, boolean explicit) {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001099 if (mUnprocessedChars != null) {
1100 moreChars = mUnprocessedChars + moreChars;
1101 }
1102 int current = 0;
1103 int len = moreChars.length();
Hans Boehm0b9806f2015-06-29 16:07:15 -07001104 boolean lastWasDigit = false;
Hans Boehm5d79d102015-09-16 16:33:47 -07001105 if (mCurrentState == CalculatorState.RESULT && len != 0) {
1106 // Clear display immediately for incomplete function name.
1107 switchToInput(KeyMaps.keyForChar(moreChars.charAt(current)));
1108 }
Hans Boehm24c91ed2016-06-30 18:53:44 -07001109 char groupingSeparator = KeyMaps.translateResult(",").charAt(0);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001110 while (current < len) {
1111 char c = moreChars.charAt(current);
Hans Boehm24c91ed2016-06-30 18:53:44 -07001112 if (Character.isSpaceChar(c) || c == groupingSeparator) {
1113 ++current;
1114 continue;
1115 }
Hans Boehm013969e2015-04-13 20:29:47 -07001116 int k = KeyMaps.keyForChar(c);
Hans Boehm0b9806f2015-06-29 16:07:15 -07001117 if (!explicit) {
1118 int expEnd;
1119 if (lastWasDigit && current !=
1120 (expEnd = Evaluator.exponentEnd(moreChars, current))) {
1121 // Process scientific notation with 'E' when pasting, in spite of ambiguity
1122 // with base of natural log.
1123 // Otherwise the 10^x key is the user's friend.
1124 mEvaluator.addExponent(moreChars, current, expEnd);
1125 current = expEnd;
1126 lastWasDigit = false;
1127 continue;
1128 } else {
1129 boolean isDigit = KeyMaps.digVal(k) != KeyMaps.NOT_DIGIT;
1130 if (current == 0 && (isDigit || k == R.id.dec_point)
1131 && mEvaluator.getExpr().hasTrailingConstant()) {
1132 // Refuse to concatenate pasted content to trailing constant.
1133 // This makes pasting of calculator results more consistent, whether or
1134 // not the old calculator instance is still around.
1135 addKeyToExpr(R.id.op_mul);
1136 }
1137 lastWasDigit = (isDigit || lastWasDigit && k == R.id.dec_point);
1138 }
1139 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001140 if (k != View.NO_ID) {
1141 mCurrentButton = findViewById(k);
Hans Boehm017de982015-06-10 17:46:03 -07001142 if (explicit) {
1143 addExplicitKeyToExpr(k);
1144 } else {
1145 addKeyToExpr(k);
1146 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001147 if (Character.isSurrogate(c)) {
1148 current += 2;
1149 } else {
1150 ++current;
1151 }
1152 continue;
1153 }
Hans Boehm013969e2015-04-13 20:29:47 -07001154 int f = KeyMaps.funForString(moreChars, current);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001155 if (f != View.NO_ID) {
1156 mCurrentButton = findViewById(f);
Hans Boehm017de982015-06-10 17:46:03 -07001157 if (explicit) {
1158 addExplicitKeyToExpr(f);
1159 } else {
1160 addKeyToExpr(f);
1161 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001162 if (f == R.id.op_sqrt) {
1163 // Square root entered as function; don't lose the parenthesis.
1164 addKeyToExpr(R.id.lparen);
1165 }
1166 current = moreChars.indexOf('(', current) + 1;
1167 continue;
1168 }
1169 // There are characters left, but we can't convert them to button presses.
1170 mUnprocessedChars = moreChars.substring(current);
1171 redisplayAfterFormulaChange();
Hans Boehm52d477a2016-04-01 17:42:50 -07001172 showOrHideToolbar();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001173 return;
1174 }
1175 mUnprocessedChars = null;
1176 redisplayAfterFormulaChange();
Hans Boehm52d477a2016-04-01 17:42:50 -07001177 showOrHideToolbar();
Hans Boehm84614952014-11-25 18:46:17 -08001178 }
1179
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001180 @Override
Justin Klaassenfc5ac822015-06-18 13:15:17 -07001181 public boolean onPaste(ClipData clip) {
1182 final ClipData.Item item = clip.getItemCount() == 0 ? null : clip.getItemAt(0);
1183 if (item == null) {
1184 // nothing to paste, bail early...
1185 return false;
1186 }
1187
1188 // Check if the item is a previously copied result, otherwise paste as raw text.
1189 final Uri uri = item.getUri();
1190 if (uri != null && mEvaluator.isLastSaved(uri)) {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001191 if (mCurrentState == CalculatorState.ERROR
Justin Klaassenfc5ac822015-06-18 13:15:17 -07001192 || mCurrentState == CalculatorState.RESULT) {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001193 setState(CalculatorState.INPUT);
1194 mEvaluator.clear();
Hans Boehm84614952014-11-25 18:46:17 -08001195 }
Hans Boehm3666e632015-07-27 18:33:12 -07001196 mEvaluator.appendSaved();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001197 redisplayAfterFormulaChange();
Justin Klaassenfc5ac822015-06-18 13:15:17 -07001198 } else {
Hans Boehm017de982015-06-10 17:46:03 -07001199 addChars(item.coerceToText(this).toString(), false);
Hans Boehm84614952014-11-25 18:46:17 -08001200 }
Justin Klaassenfc5ac822015-06-18 13:15:17 -07001201 return true;
Hans Boehm84614952014-11-25 18:46:17 -08001202 }
Chenjie Yu3937b652016-06-01 23:14:26 -07001203
1204 /**
1205 * Clean up animation for context menu.
1206 */
1207 @Override
1208 public void onContextMenuClosed(Menu menu) {
1209 stopActionModeOrContextMenu();
1210 }
Justin Klaassen4b3af052014-05-27 17:53:10 -07001211}