blob: ea1d44515471ee4a148b045e9bf6d829fb2b37b5 [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;
Annie Chin06fd3cf2016-11-07 16:04:33 -080036import android.app.FragmentManager;
Annie Chin09547532016-10-14 10:59:07 -070037import android.app.FragmentTransaction;
Justin Klaassenfc5ac822015-06-18 13:15:17 -070038import android.content.ClipData;
Hans Boehm5e6a0ca2015-09-22 17:09:01 -070039import android.content.DialogInterface;
Justin Klaassend36d63e2015-05-05 12:59:36 -070040import android.content.Intent;
Hans Boehmbfe8c222015-04-02 16:26:07 -070041import android.content.res.Resources;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070042import android.graphics.Color;
Justin Klaassen8fff1442014-06-19 10:43:29 -070043import android.graphics.Rect;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070044import android.net.Uri;
Justin Klaassen4b3af052014-05-27 17:53:10 -070045import android.os.Bundle;
Justin Klaassenf79d6f62014-08-26 12:27:08 -070046import android.support.annotation.NonNull;
Chenjie Yu3937b652016-06-01 23:14:26 -070047import android.support.v4.content.ContextCompat;
Justin Klaassen3b4d13d2014-06-06 18:18:37 +010048import android.support.v4.view.ViewPager;
Annie Chine918fd22016-03-09 11:07:54 -080049import android.text.Editable;
Hans Boehm8a4f81c2015-07-09 10:41:25 -070050import android.text.SpannableStringBuilder;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070051import android.text.Spanned;
Annie Chinf360ef02016-03-10 13:45:39 -080052import android.text.TextUtils;
Annie Chine918fd22016-03-09 11:07:54 -080053import android.text.TextWatcher;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070054import android.text.style.ForegroundColorSpan;
Justin Klaassen44595162015-05-28 17:55:20 -070055import android.util.Property;
Annie Chine918fd22016-03-09 11:07:54 -080056import android.view.ActionMode;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070057import android.view.KeyCharacterMap;
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -070058import android.view.KeyEvent;
Hans Boehm84614952014-11-25 18:46:17 -080059import android.view.Menu;
60import android.view.MenuItem;
Annie Chind0f87d22016-10-24 09:04:12 -070061import android.view.MotionEvent;
Justin Klaassen4b3af052014-05-27 17:53:10 -070062import android.view.View;
63import android.view.View.OnLongClickListener;
Justin Klaassen5f2a3342014-06-11 17:40:22 -070064import android.view.ViewAnimationUtils;
Justin Klaassen8fff1442014-06-19 10:43:29 -070065import android.view.ViewGroupOverlay;
Annie Chine918fd22016-03-09 11:07:54 -080066import android.view.ViewTreeObserver;
Justin Klaassen4b3af052014-05-27 17:53:10 -070067import android.view.animation.AccelerateDecelerateInterpolator;
Annie Chind0f87d22016-10-24 09:04:12 -070068import android.widget.FrameLayout;
Annie Chine918fd22016-03-09 11:07:54 -080069import android.widget.HorizontalScrollView;
Justin Klaassenfed941a2014-06-09 18:42:40 +010070import android.widget.TextView;
Justin Klaassend48b7562015-04-16 16:51:38 -070071import android.widget.Toolbar;
Justin Klaassenfed941a2014-06-09 18:42:40 +010072
Christine Franks7452d3a2016-10-27 13:41:18 -070073import com.android.calculator2.CalculatorFormula.OnTextSizeChangeListener;
Hans Boehm84614952014-11-25 18:46:17 -080074
75import java.io.ByteArrayInputStream;
Hans Boehm84614952014-11-25 18:46:17 -080076import java.io.ByteArrayOutputStream;
Hans Boehm84614952014-11-25 18:46:17 -080077import java.io.IOException;
Justin Klaassen721ec842015-05-28 14:30:08 -070078import java.io.ObjectInput;
79import java.io.ObjectInputStream;
80import java.io.ObjectOutput;
81import java.io.ObjectOutputStream;
Justin Klaassen4b3af052014-05-27 17:53:10 -070082
Christine Franks1d99be12016-11-14 14:00:36 -080083import static com.android.calculator2.CalculatorFormula.OnFormulaContextMenuClickListener;
84
Hans Boehm8f051c32016-10-03 16:53:58 -070085public class Calculator extends Activity
Christine Franks1d99be12016-11-14 14:00:36 -080086 implements OnTextSizeChangeListener, OnLongClickListener,
Hans Boehm8f051c32016-10-03 16:53:58 -070087 AlertDialogFragment.OnClickListener, Evaluator.EvaluationListener /* for main result */ {
Justin Klaassen2be4fdb2014-08-06 19:54:09 -070088
89 /**
90 * Constant for an invalid resource id.
91 */
92 public static final int INVALID_RES_ID = -1;
Justin Klaassen4b3af052014-05-27 17:53:10 -070093
94 private enum CalculatorState {
Hans Boehm84614952014-11-25 18:46:17 -080095 INPUT, // Result and formula both visible, no evaluation requested,
96 // Though result may be visible on bottom line.
97 EVALUATE, // Both visible, evaluation requested, evaluation/animation incomplete.
Hans Boehmc1ea0912015-06-19 15:05:07 -070098 // Not used for instant result evaluation.
Hans Boehm84614952014-11-25 18:46:17 -080099 INIT, // Very temporary state used as alternative to EVALUATE
100 // during reinitialization. Do not animate on completion.
101 ANIMATE, // Result computed, animation to enlarge result window in progress.
102 RESULT, // Result displayed, formula invisible.
103 // If we are in RESULT state, the formula was evaluated without
104 // error to initial precision.
Hans Boehm8f051c32016-10-03 16:53:58 -0700105 // The current formula is now also the last history entry.
Hans Boehm84614952014-11-25 18:46:17 -0800106 ERROR // Error displayed: Formula visible, result shows error message.
107 // Display similar to INPUT state.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700108 }
Hans Boehm84614952014-11-25 18:46:17 -0800109 // Normal transition sequence is
110 // INPUT -> EVALUATE -> ANIMATE -> RESULT (or ERROR) -> INPUT
111 // A RESULT -> ERROR transition is possible in rare corner cases, in which
112 // a higher precision evaluation exposes an error. This is possible, since we
113 // initially evaluate assuming we were given a well-defined problem. If we
114 // were actually asked to compute sqrt(<extremely tiny negative number>) we produce 0
115 // unless we are asked for enough precision that we can distinguish the argument from zero.
116 // TODO: Consider further heuristics to reduce the chance of observing this?
117 // It already seems to be observable only in contrived cases.
118 // ANIMATE, ERROR, and RESULT are translated to an INIT state if the application
119 // is restarted in that state. This leads us to recompute and redisplay the result
120 // ASAP.
121 // TODO: Possibly save a bit more information, e.g. its initial display string
122 // or most significant digit position, to speed up restart.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700123
Justin Klaassen44595162015-05-28 17:55:20 -0700124 private final Property<TextView, Integer> TEXT_COLOR =
125 new Property<TextView, Integer>(Integer.class, "textColor") {
126 @Override
127 public Integer get(TextView textView) {
128 return textView.getCurrentTextColor();
129 }
130
131 @Override
132 public void set(TextView textView, Integer textColor) {
133 textView.setTextColor(textColor);
134 }
135 };
136
Justin Klaassen3e223ea2016-02-05 14:18:06 -0800137 private static final String NAME = "Calculator";
Hans Boehm84614952014-11-25 18:46:17 -0800138 private static final String KEY_DISPLAY_STATE = NAME + "_display_state";
Hans Boehm760a9dc2015-04-20 10:27:12 -0700139 private static final String KEY_UNPROCESSED_CHARS = NAME + "_unprocessed_chars";
Justin Klaassen3e223ea2016-02-05 14:18:06 -0800140 /**
141 * Associated value is a byte array holding the evaluator state.
142 */
Hans Boehm84614952014-11-25 18:46:17 -0800143 private static final String KEY_EVAL_STATE = NAME + "_eval_state";
Justin Klaassen3e223ea2016-02-05 14:18:06 -0800144 private static final String KEY_INVERSE_MODE = NAME + "_inverse_mode";
Christine Frankseeff27f2016-07-29 12:05:29 -0700145 /**
146 * Associated value is an boolean holding the visibility state of the toolbar.
147 */
148 private static final String KEY_SHOW_TOOLBAR = NAME + "_show_toolbar";
Justin Klaassen741471e2014-06-11 09:43:44 -0700149
Annie Chine918fd22016-03-09 11:07:54 -0800150 private final ViewTreeObserver.OnPreDrawListener mPreDrawListener =
151 new ViewTreeObserver.OnPreDrawListener() {
152 @Override
153 public boolean onPreDraw() {
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700154 mFormulaContainer.scrollTo(mFormulaText.getRight(), 0);
Annie Chine918fd22016-03-09 11:07:54 -0800155 final ViewTreeObserver observer = mFormulaContainer.getViewTreeObserver();
156 if (observer.isAlive()) {
157 observer.removeOnPreDrawListener(this);
158 }
159 return false;
160 }
161 };
162
Christine Franks1d99be12016-11-14 14:00:36 -0800163 public final OnDisplayMemoryOperationsListener mOnDisplayMemoryOperationsListener =
164 new OnDisplayMemoryOperationsListener() {
165 @Override
166 public boolean shouldDisplayMemory() {
167 return mEvaluator.getMemoryIndex() != 0;
168 }
169 };
170
171 public final OnFormulaContextMenuClickListener mOnFormulaContextMenuClickListener =
172 new OnFormulaContextMenuClickListener() {
173 @Override
174 public boolean onPaste(ClipData clip) {
175 final ClipData.Item item = clip.getItemCount() == 0 ? null : clip.getItemAt(0);
176 if (item == null) {
177 // nothing to paste, bail early...
178 return false;
179 }
180
181 // Check if the item is a previously copied result, otherwise paste as raw text.
182 final Uri uri = item.getUri();
183 if (uri != null && mEvaluator.isLastSaved(uri)) {
184 clearIfNotInputState();
185 mEvaluator.appendExpr(mEvaluator.getSavedIndex());
186 redisplayAfterFormulaChange();
187 } else {
188 addChars(item.coerceToText(Calculator.this).toString(), false);
189 }
190 return true;
191 }
192
193 @Override
194 public void onMemoryRecall() {
195 clearIfNotInputState();
196 long memoryIndex = mEvaluator.getMemoryIndex();
197 if (memoryIndex != 0) {
198 mEvaluator.appendExpr(mEvaluator.getMemoryIndex());
199 redisplayAfterFormulaChange();
200 } // FIXME: Avoid the 0 case, e.g. by graying out button when memory is unavailable.
201 }
202 };
203
204
Annie Chine918fd22016-03-09 11:07:54 -0800205 private final TextWatcher mFormulaTextWatcher = new TextWatcher() {
206 @Override
207 public void beforeTextChanged(CharSequence charSequence, int start, int count, int after) {
208 }
209
210 @Override
211 public void onTextChanged(CharSequence charSequence, int start, int count, int after) {
212 }
213
214 @Override
215 public void afterTextChanged(Editable editable) {
216 final ViewTreeObserver observer = mFormulaContainer.getViewTreeObserver();
217 if (observer.isAlive()) {
218 observer.removeOnPreDrawListener(mPreDrawListener);
219 observer.addOnPreDrawListener(mPreDrawListener);
220 }
221 }
222 };
223
Annie Chind0f87d22016-10-24 09:04:12 -0700224 private final DragLayout.DragCallback mDragCallback = new DragLayout.DragCallback() {
225 @Override
226 public void onStartDragging() {
227 showHistoryFragment(FragmentTransaction.TRANSIT_NONE);
228 }
229
230 @Override
231 public void whileDragging(float yFraction) {
232 // no-op
233 }
234
235 @Override
236 public void onClosed() {
Annie Chin06fd3cf2016-11-07 16:04:33 -0800237 popFragmentBackstack();
Annie Chind0f87d22016-10-24 09:04:12 -0700238 }
239
240 @Override
241 public boolean allowDrag(MotionEvent event) {
242 return isViewTarget(mHistoryFrame, event) || isViewTarget(mDisplayView, event);
243 }
244
245 @Override
246 public boolean shouldInterceptTouchEvent(MotionEvent event) {
247 return isViewTarget(mHistoryFrame, event) || isViewTarget(mDisplayView, event);
248 }
249
250 @Override
251 public int getDisplayHeight() {
252 return mDisplayView.getMeasuredHeight();
253 }
254
255 public void onLayout(int translation) {
256 mHistoryFrame.setTranslationY(translation + mDisplayView.getBottom());
257 }
258 };
259
260 private final Rect mHitRect = new Rect();
261
Justin Klaassen4b3af052014-05-27 17:53:10 -0700262 private CalculatorState mCurrentState;
Hans Boehm84614952014-11-25 18:46:17 -0800263 private Evaluator mEvaluator;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700264
Justin Klaassen9d33cdc2016-02-21 14:16:14 -0800265 private CalculatorDisplay mDisplayView;
Justin Klaassend48b7562015-04-16 16:51:38 -0700266 private TextView mModeView;
Christine Franks7452d3a2016-10-27 13:41:18 -0700267 private CalculatorFormula mFormulaText;
Justin Klaassen44595162015-05-28 17:55:20 -0700268 private CalculatorResult mResultText;
Annie Chine918fd22016-03-09 11:07:54 -0800269 private HorizontalScrollView mFormulaContainer;
Annie Chin09547532016-10-14 10:59:07 -0700270 private DragLayout mDragLayout;
Annie Chind0f87d22016-10-24 09:04:12 -0700271 private FrameLayout mHistoryFrame;
Justin Klaassend48b7562015-04-16 16:51:38 -0700272
Justin Klaassen3b4d13d2014-06-06 18:18:37 +0100273 private ViewPager mPadViewPager;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700274 private View mDeleteButton;
275 private View mClearButton;
Justin Klaassend48b7562015-04-16 16:51:38 -0700276 private View mEqualButton;
Justin Klaassene2711cb2015-05-28 11:13:17 -0700277
278 private TextView mInverseToggle;
279 private TextView mModeToggle;
280
Justin Klaassen721ec842015-05-28 14:30:08 -0700281 private View[] mInvertibleButtons;
Justin Klaassene2711cb2015-05-28 11:13:17 -0700282 private View[] mInverseButtons;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700283
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700284 private View mCurrentButton;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700285 private Animator mCurrentAnimator;
286
Hans Boehm8a4f81c2015-07-09 10:41:25 -0700287 // Characters that were recently entered at the end of the display that have not yet
288 // been added to the underlying expression.
289 private String mUnprocessedChars = null;
290
291 // Color to highlight unprocessed characters from physical keyboard.
292 // TODO: should probably match this to the error color?
293 private ForegroundColorSpan mUnprocessedColorSpan = new ForegroundColorSpan(Color.RED);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700294
Annie Chin26e159e2016-05-18 15:17:14 -0700295 // Whether the display is one line.
296 private boolean mOneLine;
297
Annie Chin09547532016-10-14 10:59:07 -0700298 private HistoryFragment mHistoryFragment = new HistoryFragment();
299
Justin Klaassen4b3af052014-05-27 17:53:10 -0700300 @Override
301 protected void onCreate(Bundle savedInstanceState) {
302 super.onCreate(savedInstanceState);
Annie Chin09547532016-10-14 10:59:07 -0700303 setContentView(R.layout.activity_calculator_main);
Justin Klaassend48b7562015-04-16 16:51:38 -0700304 setActionBar((Toolbar) findViewById(R.id.toolbar));
305
306 // Hide all default options in the ActionBar.
307 getActionBar().setDisplayOptions(0);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700308
Justin Klaassen9d33cdc2016-02-21 14:16:14 -0800309 // Ensure the toolbar stays visible while the options menu is displayed.
310 getActionBar().addOnMenuVisibilityListener(new ActionBar.OnMenuVisibilityListener() {
311 @Override
312 public void onMenuVisibilityChanged(boolean isVisible) {
313 mDisplayView.setForceToolbarVisible(isVisible);
314 }
315 });
316
317 mDisplayView = (CalculatorDisplay) findViewById(R.id.display);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700318 mModeView = (TextView) findViewById(R.id.mode);
Christine Franks7452d3a2016-10-27 13:41:18 -0700319 mFormulaText = (CalculatorFormula) findViewById(R.id.formula);
Justin Klaassen44595162015-05-28 17:55:20 -0700320 mResultText = (CalculatorResult) findViewById(R.id.result);
Annie Chine918fd22016-03-09 11:07:54 -0800321 mFormulaContainer = (HorizontalScrollView) findViewById(R.id.formula_container);
Justin Klaassend48b7562015-04-16 16:51:38 -0700322
Justin Klaassen3b4d13d2014-06-06 18:18:37 +0100323 mPadViewPager = (ViewPager) findViewById(R.id.pad_pager);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700324 mDeleteButton = findViewById(R.id.del);
325 mClearButton = findViewById(R.id.clr);
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700326 mEqualButton = findViewById(R.id.pad_numeric).findViewById(R.id.eq);
327 if (mEqualButton == null || mEqualButton.getVisibility() != View.VISIBLE) {
328 mEqualButton = findViewById(R.id.pad_operator).findViewById(R.id.eq);
329 }
Justin Klaassene2711cb2015-05-28 11:13:17 -0700330
331 mInverseToggle = (TextView) findViewById(R.id.toggle_inv);
332 mModeToggle = (TextView) findViewById(R.id.toggle_mode);
333
Annie Chin26e159e2016-05-18 15:17:14 -0700334 mOneLine = mResultText.getVisibility() == View.INVISIBLE;
335
Justin Klaassen721ec842015-05-28 14:30:08 -0700336 mInvertibleButtons = new View[] {
337 findViewById(R.id.fun_sin),
338 findViewById(R.id.fun_cos),
Hans Boehm4db31b42015-05-31 12:19:05 -0700339 findViewById(R.id.fun_tan),
340 findViewById(R.id.fun_ln),
341 findViewById(R.id.fun_log),
342 findViewById(R.id.op_sqrt)
Justin Klaassene2711cb2015-05-28 11:13:17 -0700343 };
344 mInverseButtons = new View[] {
345 findViewById(R.id.fun_arcsin),
346 findViewById(R.id.fun_arccos),
Hans Boehm4db31b42015-05-31 12:19:05 -0700347 findViewById(R.id.fun_arctan),
348 findViewById(R.id.fun_exp),
349 findViewById(R.id.fun_10pow),
350 findViewById(R.id.op_sqr)
Justin Klaassene2711cb2015-05-28 11:13:17 -0700351 };
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700352
Annie Chin06fd3cf2016-11-07 16:04:33 -0800353 mEvaluator = Evaluator.getInstance(this);
Hans Boehm8f051c32016-10-03 16:53:58 -0700354 mResultText.setEvaluator(mEvaluator, Evaluator.MAIN_INDEX);
Hans Boehm013969e2015-04-13 20:29:47 -0700355 KeyMaps.setActivity(this);
Justin Klaassen2be4fdb2014-08-06 19:54:09 -0700356
Annie Chin09547532016-10-14 10:59:07 -0700357 mDragLayout = (DragLayout) findViewById(R.id.drag_layout);
Annie Chind0f87d22016-10-24 09:04:12 -0700358 mDragLayout.removeDragCallback(mDragCallback);
359 mDragLayout.addDragCallback(mDragCallback);
Annie Chin09547532016-10-14 10:59:07 -0700360
Annie Chind0f87d22016-10-24 09:04:12 -0700361 mHistoryFrame = (FrameLayout) findViewById(R.id.history_frame);
Annie Chin09547532016-10-14 10:59:07 -0700362
Hans Boehm84614952014-11-25 18:46:17 -0800363 if (savedInstanceState != null) {
Annie Chinbc001882016-11-09 19:41:21 -0800364 final CalculatorState savedState = CalculatorState.values()[
365 savedInstanceState.getInt(KEY_DISPLAY_STATE,
366 CalculatorState.INPUT.ordinal())];
367 setState(savedState);
Hans Boehm760a9dc2015-04-20 10:27:12 -0700368 CharSequence unprocessed = savedInstanceState.getCharSequence(KEY_UNPROCESSED_CHARS);
369 if (unprocessed != null) {
370 mUnprocessedChars = unprocessed.toString();
371 }
372 byte[] state = savedInstanceState.getByteArray(KEY_EVAL_STATE);
Hans Boehm84614952014-11-25 18:46:17 -0800373 if (state != null) {
374 try (ObjectInput in = new ObjectInputStream(new ByteArrayInputStream(state))) {
375 mEvaluator.restoreInstanceState(in);
376 } catch (Throwable ignored) {
377 // When in doubt, revert to clean state
378 mCurrentState = CalculatorState.INPUT;
Hans Boehm8f051c32016-10-03 16:53:58 -0700379 mEvaluator.clearMain();
Hans Boehm84614952014-11-25 18:46:17 -0800380 }
381 }
Hans Boehmfbcef702015-04-27 18:07:47 -0700382 } else {
383 mCurrentState = CalculatorState.INPUT;
Hans Boehm8f051c32016-10-03 16:53:58 -0700384 mEvaluator.clearMain();
Hans Boehm84614952014-11-25 18:46:17 -0800385 }
Justin Klaassene2711cb2015-05-28 11:13:17 -0700386
Christine Franks1d99be12016-11-14 14:00:36 -0800387 mFormulaText.setOnContextMenuClickListener(mOnFormulaContextMenuClickListener);
388 mFormulaText.setOnDisplayMemoryOperationsListener(mOnDisplayMemoryOperationsListener);
389
Hans Boehm08e8f322015-04-21 13:18:38 -0700390 mFormulaText.setOnTextSizeChangeListener(this);
Annie Chine918fd22016-03-09 11:07:54 -0800391 mFormulaText.addTextChangedListener(mFormulaTextWatcher);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700392 mDeleteButton.setOnLongClickListener(this);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700393
Justin Klaassen3e223ea2016-02-05 14:18:06 -0800394 onInverseToggled(savedInstanceState != null
395 && savedInstanceState.getBoolean(KEY_INVERSE_MODE));
Christine Frankseeff27f2016-07-29 12:05:29 -0700396
Hans Boehm8f051c32016-10-03 16:53:58 -0700397 onModeChanged(mEvaluator.getDegreeMode(Evaluator.MAIN_INDEX));
Christine Frankseeff27f2016-07-29 12:05:29 -0700398 if (savedInstanceState != null &&
399 savedInstanceState.getBoolean(KEY_SHOW_TOOLBAR, true) == false) {
400 mDisplayView.hideToolbar();
401 } else {
402 showAndMaybeHideToolbar();
403 }
Justin Klaassene2711cb2015-05-28 11:13:17 -0700404
Hans Boehm84614952014-11-25 18:46:17 -0800405 if (mCurrentState != CalculatorState.INPUT) {
Hans Boehmfbcef702015-04-27 18:07:47 -0700406 // Just reevaluate.
407 redisplayFormula();
Hans Boehm84614952014-11-25 18:46:17 -0800408 setState(CalculatorState.INIT);
Hans Boehmd4959e82016-11-15 18:01:28 -0800409 // Request evaluation when we know display width.
410 mResultText.setShouldRequireResult(true, this);
Hans Boehm84614952014-11-25 18:46:17 -0800411 } else {
Hans Boehmd4959e82016-11-15 18:01:28 -0800412 // This resultText will explicitly call evaluateAndNotify when ready.
413 mResultText.setShouldRequireResult(false, null);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700414 redisplayAfterFormulaChange();
Hans Boehm84614952014-11-25 18:46:17 -0800415 }
416 // TODO: We're currently not saving and restoring scroll position.
417 // We probably should. Details may require care to deal with:
418 // - new display size
419 // - slow recomputation if we've scrolled far.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700420 }
421
422 @Override
Justin Klaassen9d33cdc2016-02-21 14:16:14 -0800423 protected void onResume() {
424 super.onResume();
Christine Frankseeff27f2016-07-29 12:05:29 -0700425 if (mDisplayView.isToolbarVisible()) {
426 showAndMaybeHideToolbar();
427 }
Justin Klaassen9d33cdc2016-02-21 14:16:14 -0800428 }
429
430 @Override
Justin Klaassenf79d6f62014-08-26 12:27:08 -0700431 protected void onSaveInstanceState(@NonNull Bundle outState) {
Hans Boehm40125442016-01-22 10:35:35 -0800432 mEvaluator.cancelAll(true);
Justin Klaassenf79d6f62014-08-26 12:27:08 -0700433 // If there's an animation in progress, cancel it first to ensure our state is up-to-date.
434 if (mCurrentAnimator != null) {
435 mCurrentAnimator.cancel();
436 }
437
Justin Klaassen4b3af052014-05-27 17:53:10 -0700438 super.onSaveInstanceState(outState);
Hans Boehm84614952014-11-25 18:46:17 -0800439 outState.putInt(KEY_DISPLAY_STATE, mCurrentState.ordinal());
Hans Boehm760a9dc2015-04-20 10:27:12 -0700440 outState.putCharSequence(KEY_UNPROCESSED_CHARS, mUnprocessedChars);
Hans Boehm84614952014-11-25 18:46:17 -0800441 ByteArrayOutputStream byteArrayStream = new ByteArrayOutputStream();
442 try (ObjectOutput out = new ObjectOutputStream(byteArrayStream)) {
443 mEvaluator.saveInstanceState(out);
444 } catch (IOException e) {
445 // Impossible; No IO involved.
446 throw new AssertionError("Impossible IO exception", e);
447 }
448 outState.putByteArray(KEY_EVAL_STATE, byteArrayStream.toByteArray());
Justin Klaassen3e223ea2016-02-05 14:18:06 -0800449 outState.putBoolean(KEY_INVERSE_MODE, mInverseToggle.isSelected());
Christine Frankseeff27f2016-07-29 12:05:29 -0700450 outState.putBoolean(KEY_SHOW_TOOLBAR, mDisplayView.isToolbarVisible());
Justin Klaassen4b3af052014-05-27 17:53:10 -0700451 }
452
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700453 // Set the state, updating delete label and display colors.
454 // This restores display positions on moving to INPUT.
Justin Klaassend48b7562015-04-16 16:51:38 -0700455 // But movement/animation for moving to RESULT has already been done.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700456 private void setState(CalculatorState state) {
457 if (mCurrentState != state) {
Hans Boehm84614952014-11-25 18:46:17 -0800458 if (state == CalculatorState.INPUT) {
Hans Boehmd4959e82016-11-15 18:01:28 -0800459 // We'll explicitly request evaluation from now on.
460 mResultText.setShouldRequireResult(false, null);
Hans Boehm84614952014-11-25 18:46:17 -0800461 restoreDisplayPositions();
462 }
Justin Klaassen4b3af052014-05-27 17:53:10 -0700463 mCurrentState = state;
464
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700465 if (mCurrentState == CalculatorState.RESULT) {
466 // No longer do this for ERROR; allow mistakes to be corrected.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700467 mDeleteButton.setVisibility(View.GONE);
468 mClearButton.setVisibility(View.VISIBLE);
469 } else {
470 mDeleteButton.setVisibility(View.VISIBLE);
471 mClearButton.setVisibility(View.GONE);
472 }
473
Annie Chin26e159e2016-05-18 15:17:14 -0700474 if (mOneLine) {
475 if (mCurrentState == CalculatorState.RESULT
476 || mCurrentState == CalculatorState.EVALUATE
477 || mCurrentState == CalculatorState.ANIMATE) {
478 mFormulaText.setVisibility(View.VISIBLE);
479 mResultText.setVisibility(View.VISIBLE);
Annie Chin947d93b2016-06-14 10:18:54 -0700480 } else if (mCurrentState == CalculatorState.ERROR) {
481 mFormulaText.setVisibility(View.INVISIBLE);
482 mResultText.setVisibility(View.VISIBLE);
Annie Chin26e159e2016-05-18 15:17:14 -0700483 } else {
484 mFormulaText.setVisibility(View.VISIBLE);
485 mResultText.setVisibility(View.INVISIBLE);
486 }
487 }
488
Hans Boehm84614952014-11-25 18:46:17 -0800489 if (mCurrentState == CalculatorState.ERROR) {
Chenjie Yu3937b652016-06-01 23:14:26 -0700490 final int errorColor =
491 ContextCompat.getColor(this, R.color.calculator_error_color);
Hans Boehm08e8f322015-04-21 13:18:38 -0700492 mFormulaText.setTextColor(errorColor);
Justin Klaassen44595162015-05-28 17:55:20 -0700493 mResultText.setTextColor(errorColor);
Justin Klaassen8fff1442014-06-19 10:43:29 -0700494 getWindow().setStatusBarColor(errorColor);
Justin Klaassen44595162015-05-28 17:55:20 -0700495 } else if (mCurrentState != CalculatorState.RESULT) {
Chenjie Yu3937b652016-06-01 23:14:26 -0700496 mFormulaText.setTextColor(
497 ContextCompat.getColor(this, R.color.display_formula_text_color));
498 mResultText.setTextColor(
499 ContextCompat.getColor(this, R.color.display_result_text_color));
500 getWindow().setStatusBarColor(
501 ContextCompat.getColor(this, R.color.calculator_accent_color));
Justin Klaassen4b3af052014-05-27 17:53:10 -0700502 }
Justin Klaassend48b7562015-04-16 16:51:38 -0700503
504 invalidateOptionsMenu();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700505 }
506 }
507
Annie Chin70ac8ea2016-11-18 14:43:56 -0800508 public boolean isResultState() {
509 return mCurrentState == CalculatorState.RESULT;
510 }
511
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700512 @Override
Annie Chind0f87d22016-10-24 09:04:12 -0700513 protected void onDestroy() {
514 mDragLayout.removeDragCallback(mDragCallback);
515 super.onDestroy();
516 }
517
518 @Override
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700519 public void onActionModeStarted(ActionMode mode) {
520 super.onActionModeStarted(mode);
Christine Franks7452d3a2016-10-27 13:41:18 -0700521 if (mode.getTag() == CalculatorFormula.TAG_ACTION_MODE) {
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700522 mFormulaContainer.scrollTo(mFormulaText.getRight(), 0);
523 }
524 }
525
Chenjie Yu3937b652016-06-01 23:14:26 -0700526 /**
527 * Stop any active ActionMode or ContextMenu for copy/paste actions.
528 * Return true if there was one.
529 */
530 private boolean stopActionModeOrContextMenu() {
531 if (mResultText.stopActionModeOrContextMenu()) {
Hans Boehm1176f232015-05-11 16:26:03 -0700532 return true;
533 }
Chenjie Yu3937b652016-06-01 23:14:26 -0700534 if (mFormulaText.stopActionModeOrContextMenu()) {
Hans Boehm1176f232015-05-11 16:26:03 -0700535 return true;
536 }
537 return false;
538 }
539
Justin Klaassen4b3af052014-05-27 17:53:10 -0700540 @Override
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700541 public void onUserInteraction() {
542 super.onUserInteraction();
543
544 // If there's an animation in progress, end it immediately, so the user interaction can
545 // be handled.
546 if (mCurrentAnimator != null) {
547 mCurrentAnimator.end();
548 }
549 }
550
551 @Override
Justin Klaassen3b4d13d2014-06-06 18:18:37 +0100552 public void onBackPressed() {
Chenjie Yu3937b652016-06-01 23:14:26 -0700553 if (!stopActionModeOrContextMenu()) {
Annie Chin09547532016-10-14 10:59:07 -0700554 if (mDragLayout.isOpen()) {
Annie Chin09547532016-10-14 10:59:07 -0700555 mDragLayout.setClosed();
Annie Chin06fd3cf2016-11-07 16:04:33 -0800556 popFragmentBackstack();
Annie Chin09547532016-10-14 10:59:07 -0700557 return;
558 }
Hans Boehm1176f232015-05-11 16:26:03 -0700559 if (mPadViewPager != null && mPadViewPager.getCurrentItem() != 0) {
560 // Select the previous pad.
561 mPadViewPager.setCurrentItem(mPadViewPager.getCurrentItem() - 1);
562 } else {
563 // If the user is currently looking at the first pad (or the pad is not paged),
564 // allow the system to handle the Back button.
565 super.onBackPressed();
566 }
Justin Klaassen3b4d13d2014-06-06 18:18:37 +0100567 }
568 }
569
570 @Override
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700571 public boolean onKeyUp(int keyCode, KeyEvent event) {
Justin Klaassen83959da2016-04-06 11:55:24 -0700572 // Allow the system to handle special key codes (e.g. "BACK" or "DPAD").
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700573 switch (keyCode) {
Justin Klaassen83959da2016-04-06 11:55:24 -0700574 case KeyEvent.KEYCODE_BACK:
Christine Franksf9ba2202016-10-20 17:20:19 -0700575 case KeyEvent.KEYCODE_ESCAPE:
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700576 case KeyEvent.KEYCODE_DPAD_UP:
577 case KeyEvent.KEYCODE_DPAD_DOWN:
578 case KeyEvent.KEYCODE_DPAD_LEFT:
579 case KeyEvent.KEYCODE_DPAD_RIGHT:
580 return super.onKeyUp(keyCode, event);
581 }
582
Chenjie Yu3937b652016-06-01 23:14:26 -0700583 // Stop the action mode or context menu if it's showing.
584 stopActionModeOrContextMenu();
Justin Klaassend12e0622016-04-27 16:26:47 -0700585
Hans Boehmced295e2016-11-17 17:30:13 -0800586 // Always cancel unrequested in-progress evaluation of the main expression, so that
587 // we don't have to worry about subsequent asynchronous completion.
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700588 // Requested in-progress evaluations are handled below.
589 if (mCurrentState != CalculatorState.EVALUATE) {
Hans Boehmced295e2016-11-17 17:30:13 -0800590 mEvaluator.cancel(Evaluator.MAIN_INDEX, true);
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700591 }
592
593 switch (keyCode) {
594 case KeyEvent.KEYCODE_NUMPAD_ENTER:
595 case KeyEvent.KEYCODE_ENTER:
596 case KeyEvent.KEYCODE_DPAD_CENTER:
597 mCurrentButton = mEqualButton;
598 onEquals();
599 return true;
600 case KeyEvent.KEYCODE_DEL:
601 mCurrentButton = mDeleteButton;
602 onDelete();
603 return true;
Annie Chin56bcbf12016-09-23 17:04:22 -0700604 case KeyEvent.KEYCODE_CLEAR:
605 mCurrentButton = mClearButton;
606 onClear();
607 return true;
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700608 default:
609 cancelIfEvaluating(false);
610 final int raw = event.getKeyCharacterMap().get(keyCode, event.getMetaState());
611 if ((raw & KeyCharacterMap.COMBINING_ACCENT) != 0) {
612 return true; // discard
613 }
614 // Try to discard non-printing characters and the like.
615 // The user will have to explicitly delete other junk that gets past us.
616 if (Character.isIdentifierIgnorable(raw) || Character.isWhitespace(raw)) {
617 return true;
618 }
619 char c = (char) raw;
620 if (c == '=') {
621 mCurrentButton = mEqualButton;
622 onEquals();
623 } else {
624 addChars(String.valueOf(c), true);
625 redisplayAfterFormulaChange();
626 }
627 return true;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700628 }
629 }
630
Justin Klaassene2711cb2015-05-28 11:13:17 -0700631 /**
632 * Invoked whenever the inverse button is toggled to update the UI.
633 *
634 * @param showInverse {@code true} if inverse functions should be shown
635 */
636 private void onInverseToggled(boolean showInverse) {
Justin Klaassen3e223ea2016-02-05 14:18:06 -0800637 mInverseToggle.setSelected(showInverse);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700638 if (showInverse) {
639 mInverseToggle.setContentDescription(getString(R.string.desc_inv_on));
Justin Klaassen721ec842015-05-28 14:30:08 -0700640 for (View invertibleButton : mInvertibleButtons) {
641 invertibleButton.setVisibility(View.GONE);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700642 }
643 for (View inverseButton : mInverseButtons) {
644 inverseButton.setVisibility(View.VISIBLE);
645 }
646 } else {
647 mInverseToggle.setContentDescription(getString(R.string.desc_inv_off));
Justin Klaassen721ec842015-05-28 14:30:08 -0700648 for (View invertibleButton : mInvertibleButtons) {
649 invertibleButton.setVisibility(View.VISIBLE);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700650 }
651 for (View inverseButton : mInverseButtons) {
652 inverseButton.setVisibility(View.GONE);
653 }
654 }
655 }
656
657 /**
Christine Frankseeff27f2016-07-29 12:05:29 -0700658 * Invoked whenever the deg/rad mode may have changed to update the UI. Note that the mode has
659 * not necessarily actually changed where this is invoked.
Justin Klaassene2711cb2015-05-28 11:13:17 -0700660 *
661 * @param degreeMode {@code true} if in degree mode
662 */
663 private void onModeChanged(boolean degreeMode) {
664 if (degreeMode) {
Justin Klaassend48b7562015-04-16 16:51:38 -0700665 mModeView.setText(R.string.mode_deg);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700666 mModeView.setContentDescription(getString(R.string.desc_mode_deg));
667
668 mModeToggle.setText(R.string.mode_rad);
669 mModeToggle.setContentDescription(getString(R.string.desc_switch_rad));
Hans Boehmbfe8c222015-04-02 16:26:07 -0700670 } else {
Justin Klaassend48b7562015-04-16 16:51:38 -0700671 mModeView.setText(R.string.mode_rad);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700672 mModeView.setContentDescription(getString(R.string.desc_mode_rad));
673
674 mModeToggle.setText(R.string.mode_deg);
675 mModeToggle.setContentDescription(getString(R.string.desc_switch_deg));
Hans Boehmbfe8c222015-04-02 16:26:07 -0700676 }
677 }
Hans Boehm84614952014-11-25 18:46:17 -0800678
Annie Chin06fd3cf2016-11-07 16:04:33 -0800679 private void popFragmentBackstack() {
680 final FragmentManager manager = getFragmentManager();
681 if (manager == null || manager.isDestroyed()) {
682 return;
683 }
684 manager.popBackStack();
685 }
Hans Boehm5d79d102015-09-16 16:33:47 -0700686 /**
687 * Switch to INPUT from RESULT state in response to input of the specified button_id.
688 * View.NO_ID is treated as an incomplete function id.
689 */
690 private void switchToInput(int button_id) {
691 if (KeyMaps.isBinary(button_id) || KeyMaps.isSuffix(button_id)) {
Hans Boehm8f051c32016-10-03 16:53:58 -0700692 mEvaluator.collapse(mEvaluator.getMaxIndex() /* Most recent history entry */);
Hans Boehm5d79d102015-09-16 16:33:47 -0700693 } else {
694 announceClearedForAccessibility();
Hans Boehm8f051c32016-10-03 16:53:58 -0700695 mEvaluator.clearMain();
Hans Boehm5d79d102015-09-16 16:33:47 -0700696 }
697 setState(CalculatorState.INPUT);
698 }
699
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700700 // Add the given button id to input expression.
701 // If appropriate, clear the expression before doing so.
702 private void addKeyToExpr(int id) {
703 if (mCurrentState == CalculatorState.ERROR) {
704 setState(CalculatorState.INPUT);
705 } else if (mCurrentState == CalculatorState.RESULT) {
Hans Boehm5d79d102015-09-16 16:33:47 -0700706 switchToInput(id);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700707 }
708 if (!mEvaluator.append(id)) {
709 // TODO: Some user visible feedback?
710 }
711 }
712
Hans Boehm017de982015-06-10 17:46:03 -0700713 /**
714 * Add the given button id to input expression, assuming it was explicitly
715 * typed/touched.
716 * We perform slightly more aggressive correction than in pasted expressions.
717 */
718 private void addExplicitKeyToExpr(int id) {
719 if (mCurrentState == CalculatorState.INPUT && id == R.id.op_sub) {
Hans Boehm8f051c32016-10-03 16:53:58 -0700720 mEvaluator.getExpr(Evaluator.MAIN_INDEX).removeTrailingAdditiveOperators();
Hans Boehm017de982015-06-10 17:46:03 -0700721 }
722 addKeyToExpr(id);
723 }
724
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700725 private void redisplayAfterFormulaChange() {
726 // TODO: Could do this more incrementally.
727 redisplayFormula();
728 setState(CalculatorState.INPUT);
Hans Boehm8f051c32016-10-03 16:53:58 -0700729 mResultText.clear();
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800730 if (haveUnprocessed()) {
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800731 // Force reevaluation when text is deleted, even if expression is unchanged.
732 mEvaluator.touch();
733 } else {
Hans Boehm8f051c32016-10-03 16:53:58 -0700734 if (mEvaluator.getExpr(Evaluator.MAIN_INDEX).hasInterestingOps()) {
735 mEvaluator.evaluateAndNotify(Evaluator.MAIN_INDEX, this, mResultText);
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800736 }
Hans Boehmc023b732015-04-29 11:30:47 -0700737 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700738 }
739
Hans Boehm52d477a2016-04-01 17:42:50 -0700740 /**
741 * Show the toolbar.
742 * Automatically hide it again if it's not relevant to current formula.
743 */
744 private void showAndMaybeHideToolbar() {
745 final boolean shouldBeVisible =
746 mCurrentState == CalculatorState.INPUT && mEvaluator.hasTrigFuncs();
747 mDisplayView.showToolbar(!shouldBeVisible);
748 }
749
750 /**
751 * Display or hide the toolbar depending on calculator state.
752 */
753 private void showOrHideToolbar() {
754 final boolean shouldBeVisible =
755 mCurrentState == CalculatorState.INPUT && mEvaluator.hasTrigFuncs();
756 if (shouldBeVisible) {
757 mDisplayView.showToolbar(false);
758 } else {
759 mDisplayView.hideToolbar();
760 }
761 }
762
Justin Klaassen4b3af052014-05-27 17:53:10 -0700763 public void onButtonClick(View view) {
Hans Boehmc1ea0912015-06-19 15:05:07 -0700764 // Any animation is ended before we get here.
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700765 mCurrentButton = view;
Chenjie Yu3937b652016-06-01 23:14:26 -0700766 stopActionModeOrContextMenu();
Justin Klaassen9d33cdc2016-02-21 14:16:14 -0800767
Hans Boehmc1ea0912015-06-19 15:05:07 -0700768 // See onKey above for the rationale behind some of the behavior below:
769 if (mCurrentState != CalculatorState.EVALUATE) {
Hans Boehmced295e2016-11-17 17:30:13 -0800770 // Cancel main expression evaluations that were not specifically requested.
771 mEvaluator.cancel(Evaluator.MAIN_INDEX, true);
Hans Boehm84614952014-11-25 18:46:17 -0800772 }
Justin Klaassen9d33cdc2016-02-21 14:16:14 -0800773
Justin Klaassend48b7562015-04-16 16:51:38 -0700774 final int id = view.getId();
Hans Boehm84614952014-11-25 18:46:17 -0800775 switch (id) {
Justin Klaassen4b3af052014-05-27 17:53:10 -0700776 case R.id.eq:
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700777 onEquals();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700778 break;
779 case R.id.del:
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700780 onDelete();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700781 break;
782 case R.id.clr:
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700783 onClear();
Hans Boehm52d477a2016-04-01 17:42:50 -0700784 return; // Toolbar visibility adjusted at end of animation.
Justin Klaassene2711cb2015-05-28 11:13:17 -0700785 case R.id.toggle_inv:
786 final boolean selected = !mInverseToggle.isSelected();
787 mInverseToggle.setSelected(selected);
788 onInverseToggled(selected);
Hans Boehmc1ea0912015-06-19 15:05:07 -0700789 if (mCurrentState == CalculatorState.RESULT) {
790 mResultText.redisplay(); // In case we cancelled reevaluation.
791 }
Justin Klaassene2711cb2015-05-28 11:13:17 -0700792 break;
793 case R.id.toggle_mode:
Hans Boehmc1ea0912015-06-19 15:05:07 -0700794 cancelIfEvaluating(false);
Hans Boehm8f051c32016-10-03 16:53:58 -0700795 final boolean mode = !mEvaluator.getDegreeMode(Evaluator.MAIN_INDEX);
796 if (mCurrentState == CalculatorState.RESULT
797 && mEvaluator.getExpr(Evaluator.MAIN_INDEX).hasTrigFuncs()) {
798 // Capture current result evaluated in old mode.
799 mEvaluator.collapse(mEvaluator.getMaxIndex());
Hans Boehmbfe8c222015-04-02 16:26:07 -0700800 redisplayFormula();
801 }
802 // In input mode, we reinterpret already entered trig functions.
803 mEvaluator.setDegreeMode(mode);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700804 onModeChanged(mode);
Christine Frankseeff27f2016-07-29 12:05:29 -0700805 // Show the toolbar to highlight the mode change.
806 showAndMaybeHideToolbar();
Hans Boehmbfe8c222015-04-02 16:26:07 -0700807 setState(CalculatorState.INPUT);
Justin Klaassen44595162015-05-28 17:55:20 -0700808 mResultText.clear();
Hans Boehm8f051c32016-10-03 16:53:58 -0700809 if (!haveUnprocessed()
810 && mEvaluator.getExpr(Evaluator.MAIN_INDEX).hasInterestingOps()) {
811 mEvaluator.evaluateAndNotify(mEvaluator.MAIN_INDEX, this, mResultText);
Hans Boehmc023b732015-04-29 11:30:47 -0700812 }
Christine Frankseeff27f2016-07-29 12:05:29 -0700813 return;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700814 default:
Hans Boehmc1ea0912015-06-19 15:05:07 -0700815 cancelIfEvaluating(false);
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800816 if (haveUnprocessed()) {
817 // For consistency, append as uninterpreted characters.
818 // This may actually be useful for a left parenthesis.
819 addChars(KeyMaps.toString(this, id), true);
820 } else {
821 addExplicitKeyToExpr(id);
822 redisplayAfterFormulaChange();
823 }
Justin Klaassen4b3af052014-05-27 17:53:10 -0700824 break;
825 }
Hans Boehm52d477a2016-04-01 17:42:50 -0700826 showOrHideToolbar();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700827 }
828
Hans Boehm84614952014-11-25 18:46:17 -0800829 void redisplayFormula() {
Hans Boehm8f051c32016-10-03 16:53:58 -0700830 SpannableStringBuilder formula
831 = mEvaluator.getExpr(Evaluator.MAIN_INDEX).toSpannableStringBuilder(this);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700832 if (mUnprocessedChars != null) {
833 // Add and highlight characters we couldn't process.
Hans Boehm8a4f81c2015-07-09 10:41:25 -0700834 formula.append(mUnprocessedChars, mUnprocessedColorSpan,
835 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700836 }
Hans Boehm8a4f81c2015-07-09 10:41:25 -0700837 mFormulaText.changeTextTo(formula);
Annie Chinf360ef02016-03-10 13:45:39 -0800838 mFormulaText.setContentDescription(TextUtils.isEmpty(formula)
Justin Klaassend1831412016-07-19 21:59:10 -0700839 ? getString(R.string.desc_formula) : null);
Hans Boehm84614952014-11-25 18:46:17 -0800840 }
841
Justin Klaassen4b3af052014-05-27 17:53:10 -0700842 @Override
843 public boolean onLongClick(View view) {
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700844 mCurrentButton = view;
845
Justin Klaassen4b3af052014-05-27 17:53:10 -0700846 if (view.getId() == R.id.del) {
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700847 onClear();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700848 return true;
849 }
850 return false;
851 }
852
Hans Boehm84614952014-11-25 18:46:17 -0800853 // Initial evaluation completed successfully. Initiate display.
Hans Boehm8f051c32016-10-03 16:53:58 -0700854 public void onEvaluate(long index, int initDisplayPrec, int msd, int leastDigPos,
Hans Boehma0e45f32015-05-30 13:20:35 -0700855 String truncatedWholeNumber) {
Hans Boehm8f051c32016-10-03 16:53:58 -0700856 if (index != Evaluator.MAIN_INDEX) {
857 throw new AssertionError("Unexpected evaluation result index\n");
858 }
Annie Chin37c33b62016-11-22 14:46:28 -0800859
Justin Klaassend48b7562015-04-16 16:51:38 -0700860 // Invalidate any options that may depend on the current result.
861 invalidateOptionsMenu();
862
Hans Boehm8f051c32016-10-03 16:53:58 -0700863 mResultText.onEvaluate(index, initDisplayPrec, msd, leastDigPos, truncatedWholeNumber);
Hans Boehm61568a12015-05-18 18:25:41 -0700864 if (mCurrentState != CalculatorState.INPUT) { // in EVALUATE or INIT state
Hans Boehm84614952014-11-25 18:46:17 -0800865 onResult(mCurrentState != CalculatorState.INIT);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700866 }
Hans Boehm84614952014-11-25 18:46:17 -0800867 }
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700868
Hans Boehmc1ea0912015-06-19 15:05:07 -0700869 // Reset state to reflect evaluator cancellation. Invoked by evaluator.
Hans Boehm8f051c32016-10-03 16:53:58 -0700870 public void onCancelled(long index) {
871 // Index is Evaluator.MAIN_INDEX. We should be in EVALUATE state.
Hans Boehm84614952014-11-25 18:46:17 -0800872 setState(CalculatorState.INPUT);
Hans Boehm8f051c32016-10-03 16:53:58 -0700873 mResultText.onCancelled(index);
Hans Boehm84614952014-11-25 18:46:17 -0800874 }
875
876 // Reevaluation completed; ask result to redisplay current value.
Hans Boehm8f051c32016-10-03 16:53:58 -0700877 public void onReevaluate(long index)
Hans Boehm84614952014-11-25 18:46:17 -0800878 {
Hans Boehm8f051c32016-10-03 16:53:58 -0700879 // Index is Evaluator.MAIN_INDEX.
880 mResultText.onReevaluate(index);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700881 }
882
Justin Klaassenfed941a2014-06-09 18:42:40 +0100883 @Override
884 public void onTextSizeChanged(final TextView textView, float oldSize) {
885 if (mCurrentState != CalculatorState.INPUT) {
886 // Only animate text changes that occur from user input.
887 return;
888 }
889
890 // Calculate the values needed to perform the scale and translation animations,
891 // maintaining the same apparent baseline for the displayed text.
892 final float textScale = oldSize / textView.getTextSize();
893 final float translationX = (1.0f - textScale) *
894 (textView.getWidth() / 2.0f - textView.getPaddingEnd());
895 final float translationY = (1.0f - textScale) *
896 (textView.getHeight() / 2.0f - textView.getPaddingBottom());
897
898 final AnimatorSet animatorSet = new AnimatorSet();
899 animatorSet.playTogether(
900 ObjectAnimator.ofFloat(textView, View.SCALE_X, textScale, 1.0f),
901 ObjectAnimator.ofFloat(textView, View.SCALE_Y, textScale, 1.0f),
902 ObjectAnimator.ofFloat(textView, View.TRANSLATION_X, translationX, 0.0f),
903 ObjectAnimator.ofFloat(textView, View.TRANSLATION_Y, translationY, 0.0f));
Justin Klaassen94db7202014-06-11 11:22:31 -0700904 animatorSet.setDuration(getResources().getInteger(android.R.integer.config_mediumAnimTime));
Justin Klaassenfed941a2014-06-09 18:42:40 +0100905 animatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
906 animatorSet.start();
907 }
908
Hans Boehmc1ea0912015-06-19 15:05:07 -0700909 /**
910 * Cancel any in-progress explicitly requested evaluations.
911 * @param quiet suppress pop-up message. Explicit evaluation can change the expression
912 value, and certainly changes the display, so it seems reasonable to warn.
913 * @return true if there was such an evaluation
914 */
915 private boolean cancelIfEvaluating(boolean quiet) {
916 if (mCurrentState == CalculatorState.EVALUATE) {
Hans Boehm8f051c32016-10-03 16:53:58 -0700917 // TODO: Maybe just cancel main expression evaluation?
Hans Boehmc1ea0912015-06-19 15:05:07 -0700918 mEvaluator.cancelAll(quiet);
919 return true;
920 } else {
921 return false;
922 }
923 }
924
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800925 private boolean haveUnprocessed() {
926 return mUnprocessedChars != null && !mUnprocessedChars.isEmpty();
927 }
928
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700929 private void onEquals() {
Hans Boehm56d6e762016-06-06 11:46:29 -0700930 // Ignore if in non-INPUT state, or if there are no operators.
Justin Klaassena8075af2016-07-27 15:24:45 -0700931 if (mCurrentState == CalculatorState.INPUT) {
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800932 if (haveUnprocessed()) {
Justin Klaassena8075af2016-07-27 15:24:45 -0700933 setState(CalculatorState.EVALUATE);
Hans Boehm8f051c32016-10-03 16:53:58 -0700934 onError(Evaluator.MAIN_INDEX, R.string.error_syntax);
935 } else if (mEvaluator.getExpr(Evaluator.MAIN_INDEX).hasInterestingOps()) {
Justin Klaassena8075af2016-07-27 15:24:45 -0700936 setState(CalculatorState.EVALUATE);
Hans Boehm8f051c32016-10-03 16:53:58 -0700937 mEvaluator.requireResult(Evaluator.MAIN_INDEX, this, mResultText);
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800938 }
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700939 }
940 }
941
942 private void onDelete() {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700943 // Delete works like backspace; remove the last character or operator from the expression.
944 // Note that we handle keyboard delete exactly like the delete button. For
945 // example the delete button can be used to delete a character from an incomplete
946 // function name typed on a physical keyboard.
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700947 // This should be impossible in RESULT state.
Hans Boehmc1ea0912015-06-19 15:05:07 -0700948 // If there is an in-progress explicit evaluation, just cancel it and return.
949 if (cancelIfEvaluating(false)) return;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700950 setState(CalculatorState.INPUT);
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800951 if (haveUnprocessed()) {
952 mUnprocessedChars = mUnprocessedChars.substring(0, mUnprocessedChars.length() - 1);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700953 } else {
Hans Boehmc023b732015-04-29 11:30:47 -0700954 mEvaluator.delete();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700955 }
Hans Boehm8f051c32016-10-03 16:53:58 -0700956 if (mEvaluator.getExpr(Evaluator.MAIN_INDEX).isEmpty() && !haveUnprocessed()) {
Hans Boehmdb6f9992015-08-19 12:32:56 -0700957 // Resulting formula won't be announced, since it's empty.
958 announceClearedForAccessibility();
959 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700960 redisplayAfterFormulaChange();
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700961 }
962
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700963 private void reveal(View sourceView, int colorRes, AnimatorListener listener) {
Justin Klaassen06360f92014-08-28 11:08:44 -0700964 final ViewGroupOverlay groupOverlay =
965 (ViewGroupOverlay) getWindow().getDecorView().getOverlay();
Justin Klaassen8fff1442014-06-19 10:43:29 -0700966
967 final Rect displayRect = new Rect();
Justin Klaassen06360f92014-08-28 11:08:44 -0700968 mDisplayView.getGlobalVisibleRect(displayRect);
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700969
970 // Make reveal cover the display and status bar.
971 final View revealView = new View(this);
Justin Klaassen8fff1442014-06-19 10:43:29 -0700972 revealView.setBottom(displayRect.bottom);
973 revealView.setLeft(displayRect.left);
974 revealView.setRight(displayRect.right);
Chenjie Yu3937b652016-06-01 23:14:26 -0700975 revealView.setBackgroundColor(ContextCompat.getColor(this, colorRes));
Justin Klaassen06360f92014-08-28 11:08:44 -0700976 groupOverlay.add(revealView);
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700977
Justin Klaassen4b3af052014-05-27 17:53:10 -0700978 final int[] clearLocation = new int[2];
979 sourceView.getLocationInWindow(clearLocation);
980 clearLocation[0] += sourceView.getWidth() / 2;
981 clearLocation[1] += sourceView.getHeight() / 2;
982
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700983 final int revealCenterX = clearLocation[0] - revealView.getLeft();
984 final int revealCenterY = clearLocation[1] - revealView.getTop();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700985
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700986 final double x1_2 = Math.pow(revealView.getLeft() - revealCenterX, 2);
987 final double x2_2 = Math.pow(revealView.getRight() - revealCenterX, 2);
988 final double y_2 = Math.pow(revealView.getTop() - revealCenterY, 2);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700989 final float revealRadius = (float) Math.max(Math.sqrt(x1_2 + y_2), Math.sqrt(x2_2 + y_2));
990
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700991 final Animator revealAnimator =
992 ViewAnimationUtils.createCircularReveal(revealView,
ztenghui3d6ecaf2014-06-05 09:56:00 -0700993 revealCenterX, revealCenterY, 0.0f, revealRadius);
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700994 revealAnimator.setDuration(
Justin Klaassen4b3af052014-05-27 17:53:10 -0700995 getResources().getInteger(android.R.integer.config_longAnimTime));
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700996 revealAnimator.addListener(listener);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700997
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700998 final Animator alphaAnimator = ObjectAnimator.ofFloat(revealView, View.ALPHA, 0.0f);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700999 alphaAnimator.setDuration(
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001000 getResources().getInteger(android.R.integer.config_mediumAnimTime));
Justin Klaassen4b3af052014-05-27 17:53:10 -07001001
1002 final AnimatorSet animatorSet = new AnimatorSet();
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001003 animatorSet.play(revealAnimator).before(alphaAnimator);
Justin Klaassen4b3af052014-05-27 17:53:10 -07001004 animatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
1005 animatorSet.addListener(new AnimatorListenerAdapter() {
1006 @Override
Justin Klaassen4b3af052014-05-27 17:53:10 -07001007 public void onAnimationEnd(Animator animator) {
Justin Klaassen8fff1442014-06-19 10:43:29 -07001008 groupOverlay.remove(revealView);
Justin Klaassen4b3af052014-05-27 17:53:10 -07001009 mCurrentAnimator = null;
1010 }
1011 });
1012
1013 mCurrentAnimator = animatorSet;
1014 animatorSet.start();
1015 }
1016
Hans Boehmdb6f9992015-08-19 12:32:56 -07001017 private void announceClearedForAccessibility() {
1018 mResultText.announceForAccessibility(getResources().getString(R.string.cleared));
Hans Boehmccc55662015-07-07 14:16:59 -07001019 }
1020
Hans Boehm9db3ee22016-11-18 10:09:47 -08001021 public void onClearAnimationEnd() {
1022 mUnprocessedChars = null;
1023 mResultText.clear();
1024 mEvaluator.clearMain();
1025 setState(CalculatorState.INPUT);
1026 redisplayFormula();
1027 }
1028
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001029 private void onClear() {
Hans Boehm8f051c32016-10-03 16:53:58 -07001030 if (mEvaluator.getExpr(Evaluator.MAIN_INDEX).isEmpty() && !haveUnprocessed()) {
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001031 return;
1032 }
Hans Boehmc1ea0912015-06-19 15:05:07 -07001033 cancelIfEvaluating(true);
Hans Boehmdb6f9992015-08-19 12:32:56 -07001034 announceClearedForAccessibility();
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001035 reveal(mCurrentButton, R.color.calculator_accent_color, new AnimatorListenerAdapter() {
1036 @Override
1037 public void onAnimationEnd(Animator animation) {
Hans Boehm9db3ee22016-11-18 10:09:47 -08001038 onClearAnimationEnd();
Hans Boehm52d477a2016-04-01 17:42:50 -07001039 showOrHideToolbar();
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001040 }
1041 });
1042 }
1043
Hans Boehm84614952014-11-25 18:46:17 -08001044 // Evaluation encountered en error. Display the error.
Hans Boehm8f051c32016-10-03 16:53:58 -07001045 @Override
1046 public void onError(final long index, final int errorResourceId) {
1047 if (index != Evaluator.MAIN_INDEX) {
1048 throw new AssertionError("Unexpected error source");
1049 }
Hans Boehmfbcef702015-04-27 18:07:47 -07001050 if (mCurrentState == CalculatorState.EVALUATE) {
1051 setState(CalculatorState.ANIMATE);
Hans Boehmccc55662015-07-07 14:16:59 -07001052 mResultText.announceForAccessibility(getResources().getString(errorResourceId));
Hans Boehmfbcef702015-04-27 18:07:47 -07001053 reveal(mCurrentButton, R.color.calculator_error_color,
1054 new AnimatorListenerAdapter() {
1055 @Override
1056 public void onAnimationEnd(Animator animation) {
1057 setState(CalculatorState.ERROR);
Hans Boehm8f051c32016-10-03 16:53:58 -07001058 mResultText.onError(index, errorResourceId);
Hans Boehmfbcef702015-04-27 18:07:47 -07001059 }
1060 });
1061 } else if (mCurrentState == CalculatorState.INIT) {
1062 setState(CalculatorState.ERROR);
Hans Boehm8f051c32016-10-03 16:53:58 -07001063 mResultText.onError(index, errorResourceId);
Hans Boehmc023b732015-04-29 11:30:47 -07001064 } else {
Justin Klaassen44595162015-05-28 17:55:20 -07001065 mResultText.clear();
Justin Klaassen2be4fdb2014-08-06 19:54:09 -07001066 }
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001067 }
1068
Hans Boehm84614952014-11-25 18:46:17 -08001069 // Animate movement of result into the top formula slot.
1070 // Result window now remains translated in the top slot while the result is displayed.
1071 // (We convert it back to formula use only when the user provides new input.)
Justin Klaassen44595162015-05-28 17:55:20 -07001072 // Historical note: In the Lollipop version, this invisibly and instantaneously moved
Hans Boehm84614952014-11-25 18:46:17 -08001073 // formula and result displays back at the end of the animation. We no longer do that,
1074 // so that we can continue to properly support scrolling of the result.
1075 // We assume the result already contains the text to be expanded.
1076 private void onResult(boolean animate) {
Justin Klaassen44595162015-05-28 17:55:20 -07001077 // Calculate the textSize that would be used to display the result in the formula.
1078 // For scrollable results just use the minimum textSize to maximize the number of digits
1079 // that are visible on screen.
1080 float textSize = mFormulaText.getMinimumTextSize();
1081 if (!mResultText.isScrollable()) {
1082 textSize = mFormulaText.getVariableTextSize(mResultText.getText().toString());
1083 }
1084
1085 // Scale the result to match the calculated textSize, minimizing the jump-cut transition
1086 // when a result is reused in a subsequent expression.
1087 final float resultScale = textSize / mResultText.getTextSize();
1088
1089 // Set the result's pivot to match its gravity.
1090 mResultText.setPivotX(mResultText.getWidth() - mResultText.getPaddingRight());
1091 mResultText.setPivotY(mResultText.getHeight() - mResultText.getPaddingBottom());
1092
1093 // Calculate the necessary translations so the result takes the place of the formula and
1094 // the formula moves off the top of the screen.
Annie Chin28589dc2016-06-09 17:50:51 -07001095 final float resultTranslationY = (mFormulaContainer.getBottom() - mResultText.getBottom())
1096 - (mFormulaText.getPaddingBottom() - mResultText.getPaddingBottom());
1097 float formulaTranslationY = -mFormulaContainer.getBottom();
Annie Chin26e159e2016-05-18 15:17:14 -07001098 if (mOneLine) {
1099 // Position the result text.
1100 mResultText.setY(mResultText.getBottom());
Annie Chin28589dc2016-06-09 17:50:51 -07001101 formulaTranslationY = -(findViewById(R.id.toolbar).getBottom()
1102 + mFormulaContainer.getBottom());
Annie Chin26e159e2016-05-18 15:17:14 -07001103 }
Justin Klaassen4b3af052014-05-27 17:53:10 -07001104
Justin Klaassen44595162015-05-28 17:55:20 -07001105 // Change the result's textColor to match the formula.
1106 final int formulaTextColor = mFormulaText.getCurrentTextColor();
1107
Hans Boehm84614952014-11-25 18:46:17 -08001108 if (animate) {
Hans Boehmccc55662015-07-07 14:16:59 -07001109 mResultText.announceForAccessibility(getResources().getString(R.string.desc_eq));
1110 mResultText.announceForAccessibility(mResultText.getText());
Hans Boehmc1ea0912015-06-19 15:05:07 -07001111 setState(CalculatorState.ANIMATE);
Hans Boehm84614952014-11-25 18:46:17 -08001112 final AnimatorSet animatorSet = new AnimatorSet();
1113 animatorSet.playTogether(
Justin Klaassen44595162015-05-28 17:55:20 -07001114 ObjectAnimator.ofPropertyValuesHolder(mResultText,
1115 PropertyValuesHolder.ofFloat(View.SCALE_X, resultScale),
1116 PropertyValuesHolder.ofFloat(View.SCALE_Y, resultScale),
1117 PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, resultTranslationY)),
1118 ObjectAnimator.ofArgb(mResultText, TEXT_COLOR, formulaTextColor),
Annie Chine918fd22016-03-09 11:07:54 -08001119 ObjectAnimator.ofFloat(mFormulaContainer, View.TRANSLATION_Y,
1120 formulaTranslationY));
Justin Klaassen44595162015-05-28 17:55:20 -07001121 animatorSet.setDuration(getResources().getInteger(
1122 android.R.integer.config_longAnimTime));
Hans Boehm84614952014-11-25 18:46:17 -08001123 animatorSet.addListener(new AnimatorListenerAdapter() {
1124 @Override
Hans Boehm84614952014-11-25 18:46:17 -08001125 public void onAnimationEnd(Animator animation) {
Hans Boehm8f051c32016-10-03 16:53:58 -07001126 // Add current result to history.
1127 mEvaluator.preserve(true);
Hans Boehm84614952014-11-25 18:46:17 -08001128 setState(CalculatorState.RESULT);
1129 mCurrentAnimator = null;
1130 }
1131 });
Justin Klaassen4b3af052014-05-27 17:53:10 -07001132
Hans Boehm84614952014-11-25 18:46:17 -08001133 mCurrentAnimator = animatorSet;
1134 animatorSet.start();
Hans Boehm8f051c32016-10-03 16:53:58 -07001135 } else /* No animation desired; get there fast when restarting */ {
Justin Klaassen44595162015-05-28 17:55:20 -07001136 mResultText.setScaleX(resultScale);
1137 mResultText.setScaleY(resultScale);
1138 mResultText.setTranslationY(resultTranslationY);
1139 mResultText.setTextColor(formulaTextColor);
Annie Chine918fd22016-03-09 11:07:54 -08001140 mFormulaContainer.setTranslationY(formulaTranslationY);
Hans Boehm8f051c32016-10-03 16:53:58 -07001141 mEvaluator.represerve();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001142 setState(CalculatorState.RESULT);
Hans Boehm84614952014-11-25 18:46:17 -08001143 }
Justin Klaassen4b3af052014-05-27 17:53:10 -07001144 }
Hans Boehm84614952014-11-25 18:46:17 -08001145
1146 // Restore positions of the formula and result displays back to their original,
1147 // pre-animation state.
1148 private void restoreDisplayPositions() {
1149 // Clear result.
Justin Klaassen44595162015-05-28 17:55:20 -07001150 mResultText.setText("");
Hans Boehm84614952014-11-25 18:46:17 -08001151 // Reset all of the values modified during the animation.
Justin Klaassen44595162015-05-28 17:55:20 -07001152 mResultText.setScaleX(1.0f);
1153 mResultText.setScaleY(1.0f);
1154 mResultText.setTranslationX(0.0f);
1155 mResultText.setTranslationY(0.0f);
Annie Chine918fd22016-03-09 11:07:54 -08001156 mFormulaContainer.setTranslationY(0.0f);
Hans Boehm84614952014-11-25 18:46:17 -08001157
Hans Boehm08e8f322015-04-21 13:18:38 -07001158 mFormulaText.requestFocus();
Hans Boehm5e6a0ca2015-09-22 17:09:01 -07001159 }
1160
1161 @Override
1162 public void onClick(AlertDialogFragment fragment, int which) {
1163 if (which == DialogInterface.BUTTON_POSITIVE) {
1164 // Timeout extension request.
Hans Boehm8f051c32016-10-03 16:53:58 -07001165 mEvaluator.setLongTimeout();
Hans Boehm5e6a0ca2015-09-22 17:09:01 -07001166 }
1167 }
Hans Boehm84614952014-11-25 18:46:17 -08001168
Justin Klaassend48b7562015-04-16 16:51:38 -07001169 @Override
1170 public boolean onCreateOptionsMenu(Menu menu) {
Justin Klaassend36d63e2015-05-05 12:59:36 -07001171 super.onCreateOptionsMenu(menu);
1172
1173 getMenuInflater().inflate(R.menu.activity_calculator, menu);
Justin Klaassend48b7562015-04-16 16:51:38 -07001174 return true;
1175 }
1176
1177 @Override
1178 public boolean onPrepareOptionsMenu(Menu menu) {
Justin Klaassend36d63e2015-05-05 12:59:36 -07001179 super.onPrepareOptionsMenu(menu);
1180
1181 // Show the leading option when displaying a result.
1182 menu.findItem(R.id.menu_leading).setVisible(mCurrentState == CalculatorState.RESULT);
1183
1184 // Show the fraction option when displaying a rational result.
1185 menu.findItem(R.id.menu_fraction).setVisible(mCurrentState == CalculatorState.RESULT
Hans Boehm8f051c32016-10-03 16:53:58 -07001186 && mEvaluator.getResult(Evaluator.MAIN_INDEX).exactlyDisplayable());
Justin Klaassend36d63e2015-05-05 12:59:36 -07001187
Justin Klaassend48b7562015-04-16 16:51:38 -07001188 return true;
Hans Boehm84614952014-11-25 18:46:17 -08001189 }
1190
1191 @Override
Justin Klaassend48b7562015-04-16 16:51:38 -07001192 public boolean onOptionsItemSelected(MenuItem item) {
Hans Boehm84614952014-11-25 18:46:17 -08001193 switch (item.getItemId()) {
Annie Chinabd202f2016-10-14 14:23:45 -07001194 case R.id.menu_history:
Annie Chin09547532016-10-14 10:59:07 -07001195 showHistoryFragment(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
1196 mDragLayout.setOpen();
Annie Chinabd202f2016-10-14 14:23:45 -07001197 return true;
Justin Klaassend36d63e2015-05-05 12:59:36 -07001198 case R.id.menu_leading:
1199 displayFull();
Hans Boehm84614952014-11-25 18:46:17 -08001200 return true;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001201 case R.id.menu_fraction:
1202 displayFraction();
1203 return true;
Justin Klaassend36d63e2015-05-05 12:59:36 -07001204 case R.id.menu_licenses:
1205 startActivity(new Intent(this, Licenses.class));
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001206 return true;
Hans Boehm84614952014-11-25 18:46:17 -08001207 default:
1208 return super.onOptionsItemSelected(item);
1209 }
1210 }
1211
Annie Chin09547532016-10-14 10:59:07 -07001212 private void showHistoryFragment(int transit) {
Annie Chin06fd3cf2016-11-07 16:04:33 -08001213 final FragmentManager manager = getFragmentManager();
1214 if (manager == null || manager.isDestroyed()) {
1215 return;
1216 }
Annie Chind0f87d22016-10-24 09:04:12 -07001217 if (!mDragLayout.isOpen()) {
1218 getFragmentManager().beginTransaction()
1219 .replace(R.id.history_frame, mHistoryFragment, HistoryFragment.TAG)
1220 .setTransition(transit)
1221 .addToBackStack(HistoryFragment.TAG)
1222 .commit();
1223 }
Annie Chin06fd3cf2016-11-07 16:04:33 -08001224 // TODO: pass current scroll position of result
Annie Chin09547532016-10-14 10:59:07 -07001225 }
1226
Christine Franks7452d3a2016-10-27 13:41:18 -07001227 private void displayMessage(String title, String message) {
1228 AlertDialogFragment.showMessageDialog(this, title, message, null);
Hans Boehm84614952014-11-25 18:46:17 -08001229 }
1230
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001231 private void displayFraction() {
Hans Boehm8f051c32016-10-03 16:53:58 -07001232 UnifiedReal result = mEvaluator.getResult(Evaluator.MAIN_INDEX);
Christine Franks7452d3a2016-10-27 13:41:18 -07001233 displayMessage(getString(R.string.menu_fraction),
1234 KeyMaps.translateResult(result.toNiceString()));
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001235 }
1236
1237 // Display full result to currently evaluated precision
1238 private void displayFull() {
1239 Resources res = getResources();
Hans Boehm24c91ed2016-06-30 18:53:44 -07001240 String msg = mResultText.getFullText(true /* withSeparators */) + " ";
Justin Klaassen44595162015-05-28 17:55:20 -07001241 if (mResultText.fullTextIsExact()) {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001242 msg += res.getString(R.string.exact);
1243 } else {
1244 msg += res.getString(R.string.approximate);
1245 }
Christine Franks7452d3a2016-10-27 13:41:18 -07001246 displayMessage(getString(R.string.menu_leading), msg);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001247 }
1248
Hans Boehm017de982015-06-10 17:46:03 -07001249 /**
1250 * Add input characters to the end of the expression.
1251 * Map them to the appropriate button pushes when possible. Leftover characters
1252 * are added to mUnprocessedChars, which is presumed to immediately precede the newly
1253 * added characters.
Hans Boehm65a99a42016-02-03 18:16:07 -08001254 * @param moreChars characters to be added
1255 * @param explicit these characters were explicitly typed by the user, not pasted
Hans Boehm017de982015-06-10 17:46:03 -07001256 */
1257 private void addChars(String moreChars, boolean explicit) {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001258 if (mUnprocessedChars != null) {
1259 moreChars = mUnprocessedChars + moreChars;
1260 }
1261 int current = 0;
1262 int len = moreChars.length();
Hans Boehm0b9806f2015-06-29 16:07:15 -07001263 boolean lastWasDigit = false;
Hans Boehm5d79d102015-09-16 16:33:47 -07001264 if (mCurrentState == CalculatorState.RESULT && len != 0) {
1265 // Clear display immediately for incomplete function name.
1266 switchToInput(KeyMaps.keyForChar(moreChars.charAt(current)));
1267 }
Hans Boehm24c91ed2016-06-30 18:53:44 -07001268 char groupingSeparator = KeyMaps.translateResult(",").charAt(0);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001269 while (current < len) {
1270 char c = moreChars.charAt(current);
Hans Boehm24c91ed2016-06-30 18:53:44 -07001271 if (Character.isSpaceChar(c) || c == groupingSeparator) {
1272 ++current;
1273 continue;
1274 }
Hans Boehm013969e2015-04-13 20:29:47 -07001275 int k = KeyMaps.keyForChar(c);
Hans Boehm0b9806f2015-06-29 16:07:15 -07001276 if (!explicit) {
1277 int expEnd;
1278 if (lastWasDigit && current !=
1279 (expEnd = Evaluator.exponentEnd(moreChars, current))) {
1280 // Process scientific notation with 'E' when pasting, in spite of ambiguity
1281 // with base of natural log.
1282 // Otherwise the 10^x key is the user's friend.
1283 mEvaluator.addExponent(moreChars, current, expEnd);
1284 current = expEnd;
1285 lastWasDigit = false;
1286 continue;
1287 } else {
1288 boolean isDigit = KeyMaps.digVal(k) != KeyMaps.NOT_DIGIT;
1289 if (current == 0 && (isDigit || k == R.id.dec_point)
Hans Boehm8f051c32016-10-03 16:53:58 -07001290 && mEvaluator.getExpr(Evaluator.MAIN_INDEX).hasTrailingConstant()) {
Hans Boehm0b9806f2015-06-29 16:07:15 -07001291 // Refuse to concatenate pasted content to trailing constant.
1292 // This makes pasting of calculator results more consistent, whether or
1293 // not the old calculator instance is still around.
1294 addKeyToExpr(R.id.op_mul);
1295 }
1296 lastWasDigit = (isDigit || lastWasDigit && k == R.id.dec_point);
1297 }
1298 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001299 if (k != View.NO_ID) {
1300 mCurrentButton = findViewById(k);
Hans Boehm017de982015-06-10 17:46:03 -07001301 if (explicit) {
1302 addExplicitKeyToExpr(k);
1303 } else {
1304 addKeyToExpr(k);
1305 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001306 if (Character.isSurrogate(c)) {
1307 current += 2;
1308 } else {
1309 ++current;
1310 }
1311 continue;
1312 }
Hans Boehm013969e2015-04-13 20:29:47 -07001313 int f = KeyMaps.funForString(moreChars, current);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001314 if (f != View.NO_ID) {
1315 mCurrentButton = findViewById(f);
Hans Boehm017de982015-06-10 17:46:03 -07001316 if (explicit) {
1317 addExplicitKeyToExpr(f);
1318 } else {
1319 addKeyToExpr(f);
1320 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001321 if (f == R.id.op_sqrt) {
1322 // Square root entered as function; don't lose the parenthesis.
1323 addKeyToExpr(R.id.lparen);
1324 }
1325 current = moreChars.indexOf('(', current) + 1;
1326 continue;
1327 }
1328 // There are characters left, but we can't convert them to button presses.
1329 mUnprocessedChars = moreChars.substring(current);
1330 redisplayAfterFormulaChange();
Hans Boehm52d477a2016-04-01 17:42:50 -07001331 showOrHideToolbar();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001332 return;
1333 }
1334 mUnprocessedChars = null;
1335 redisplayAfterFormulaChange();
Hans Boehm52d477a2016-04-01 17:42:50 -07001336 showOrHideToolbar();
Hans Boehm84614952014-11-25 18:46:17 -08001337 }
1338
Hans Boehm8f051c32016-10-03 16:53:58 -07001339 private void clearIfNotInputState() {
1340 if (mCurrentState == CalculatorState.ERROR
1341 || mCurrentState == CalculatorState.RESULT) {
1342 setState(CalculatorState.INPUT);
1343 mEvaluator.clearMain();
1344 }
1345 }
1346
Annie Chind0f87d22016-10-24 09:04:12 -07001347 private boolean isViewTarget(View view, MotionEvent event) {
1348 mHitRect.set(0, 0, view.getWidth(), view.getHeight());
1349 mDragLayout.offsetDescendantRectToMyCoords(view, mHitRect);
1350 return mHitRect.contains((int) event.getX(), (int) event.getY());
1351 }
1352
Chenjie Yu3937b652016-06-01 23:14:26 -07001353 /**
1354 * Clean up animation for context menu.
1355 */
1356 @Override
1357 public void onContextMenuClosed(Menu menu) {
1358 stopActionModeOrContextMenu();
1359 }
Christine Franks1d99be12016-11-14 14:00:36 -08001360
1361 public interface OnDisplayMemoryOperationsListener {
1362 boolean shouldDisplayMemory();
1363 }
Justin Klaassen4b3af052014-05-27 17:53:10 -07001364}