blob: f41d87e83cdb61106468d3e2b028000027c655ef [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
Hans Boehm8f051c32016-10-03 16:53:58 -070083public class Calculator extends Activity
84 implements OnTextSizeChangeListener, OnLongClickListener, CalculatorFormula.OnPasteListener,
85 AlertDialogFragment.OnClickListener, Evaluator.EvaluationListener /* for main result */ {
Justin Klaassen2be4fdb2014-08-06 19:54:09 -070086
87 /**
88 * Constant for an invalid resource id.
89 */
90 public static final int INVALID_RES_ID = -1;
Justin Klaassen4b3af052014-05-27 17:53:10 -070091
92 private enum CalculatorState {
Hans Boehm84614952014-11-25 18:46:17 -080093 INPUT, // Result and formula both visible, no evaluation requested,
94 // Though result may be visible on bottom line.
95 EVALUATE, // Both visible, evaluation requested, evaluation/animation incomplete.
Hans Boehmc1ea0912015-06-19 15:05:07 -070096 // Not used for instant result evaluation.
Hans Boehm84614952014-11-25 18:46:17 -080097 INIT, // Very temporary state used as alternative to EVALUATE
98 // during reinitialization. Do not animate on completion.
99 ANIMATE, // Result computed, animation to enlarge result window in progress.
100 RESULT, // Result displayed, formula invisible.
101 // If we are in RESULT state, the formula was evaluated without
102 // error to initial precision.
Hans Boehm8f051c32016-10-03 16:53:58 -0700103 // The current formula is now also the last history entry.
Hans Boehm84614952014-11-25 18:46:17 -0800104 ERROR // Error displayed: Formula visible, result shows error message.
105 // Display similar to INPUT state.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700106 }
Hans Boehm84614952014-11-25 18:46:17 -0800107 // Normal transition sequence is
108 // INPUT -> EVALUATE -> ANIMATE -> RESULT (or ERROR) -> INPUT
109 // A RESULT -> ERROR transition is possible in rare corner cases, in which
110 // a higher precision evaluation exposes an error. This is possible, since we
111 // initially evaluate assuming we were given a well-defined problem. If we
112 // were actually asked to compute sqrt(<extremely tiny negative number>) we produce 0
113 // unless we are asked for enough precision that we can distinguish the argument from zero.
114 // TODO: Consider further heuristics to reduce the chance of observing this?
115 // It already seems to be observable only in contrived cases.
116 // ANIMATE, ERROR, and RESULT are translated to an INIT state if the application
117 // is restarted in that state. This leads us to recompute and redisplay the result
118 // ASAP.
119 // TODO: Possibly save a bit more information, e.g. its initial display string
120 // or most significant digit position, to speed up restart.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700121
Justin Klaassen44595162015-05-28 17:55:20 -0700122 private final Property<TextView, Integer> TEXT_COLOR =
123 new Property<TextView, Integer>(Integer.class, "textColor") {
124 @Override
125 public Integer get(TextView textView) {
126 return textView.getCurrentTextColor();
127 }
128
129 @Override
130 public void set(TextView textView, Integer textColor) {
131 textView.setTextColor(textColor);
132 }
133 };
134
Justin Klaassen3e223ea2016-02-05 14:18:06 -0800135 private static final String NAME = "Calculator";
Hans Boehm84614952014-11-25 18:46:17 -0800136 private static final String KEY_DISPLAY_STATE = NAME + "_display_state";
Hans Boehm760a9dc2015-04-20 10:27:12 -0700137 private static final String KEY_UNPROCESSED_CHARS = NAME + "_unprocessed_chars";
Justin Klaassen3e223ea2016-02-05 14:18:06 -0800138 /**
139 * Associated value is a byte array holding the evaluator state.
140 */
Hans Boehm84614952014-11-25 18:46:17 -0800141 private static final String KEY_EVAL_STATE = NAME + "_eval_state";
Justin Klaassen3e223ea2016-02-05 14:18:06 -0800142 private static final String KEY_INVERSE_MODE = NAME + "_inverse_mode";
Christine Frankseeff27f2016-07-29 12:05:29 -0700143 /**
144 * Associated value is an boolean holding the visibility state of the toolbar.
145 */
146 private static final String KEY_SHOW_TOOLBAR = NAME + "_show_toolbar";
Justin Klaassen741471e2014-06-11 09:43:44 -0700147
Annie Chine918fd22016-03-09 11:07:54 -0800148 private final ViewTreeObserver.OnPreDrawListener mPreDrawListener =
149 new ViewTreeObserver.OnPreDrawListener() {
150 @Override
151 public boolean onPreDraw() {
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700152 mFormulaContainer.scrollTo(mFormulaText.getRight(), 0);
Annie Chine918fd22016-03-09 11:07:54 -0800153 final ViewTreeObserver observer = mFormulaContainer.getViewTreeObserver();
154 if (observer.isAlive()) {
155 observer.removeOnPreDrawListener(this);
156 }
157 return false;
158 }
159 };
160
161 private final TextWatcher mFormulaTextWatcher = new TextWatcher() {
162 @Override
163 public void beforeTextChanged(CharSequence charSequence, int start, int count, int after) {
164 }
165
166 @Override
167 public void onTextChanged(CharSequence charSequence, int start, int count, int after) {
168 }
169
170 @Override
171 public void afterTextChanged(Editable editable) {
172 final ViewTreeObserver observer = mFormulaContainer.getViewTreeObserver();
173 if (observer.isAlive()) {
174 observer.removeOnPreDrawListener(mPreDrawListener);
175 observer.addOnPreDrawListener(mPreDrawListener);
176 }
177 }
178 };
179
Annie Chind0f87d22016-10-24 09:04:12 -0700180 private final DragLayout.DragCallback mDragCallback = new DragLayout.DragCallback() {
181 @Override
182 public void onStartDragging() {
183 showHistoryFragment(FragmentTransaction.TRANSIT_NONE);
184 }
185
186 @Override
187 public void whileDragging(float yFraction) {
188 // no-op
189 }
190
191 @Override
192 public void onClosed() {
Annie Chin06fd3cf2016-11-07 16:04:33 -0800193 popFragmentBackstack();
Annie Chind0f87d22016-10-24 09:04:12 -0700194 }
195
196 @Override
197 public boolean allowDrag(MotionEvent event) {
198 return isViewTarget(mHistoryFrame, event) || isViewTarget(mDisplayView, event);
199 }
200
201 @Override
202 public boolean shouldInterceptTouchEvent(MotionEvent event) {
203 return isViewTarget(mHistoryFrame, event) || isViewTarget(mDisplayView, event);
204 }
205
206 @Override
207 public int getDisplayHeight() {
208 return mDisplayView.getMeasuredHeight();
209 }
210
211 public void onLayout(int translation) {
212 mHistoryFrame.setTranslationY(translation + mDisplayView.getBottom());
213 }
214 };
215
216 private final Rect mHitRect = new Rect();
217
Justin Klaassen4b3af052014-05-27 17:53:10 -0700218 private CalculatorState mCurrentState;
Hans Boehm84614952014-11-25 18:46:17 -0800219 private Evaluator mEvaluator;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700220
Justin Klaassen9d33cdc2016-02-21 14:16:14 -0800221 private CalculatorDisplay mDisplayView;
Justin Klaassend48b7562015-04-16 16:51:38 -0700222 private TextView mModeView;
Christine Franks7452d3a2016-10-27 13:41:18 -0700223 private CalculatorFormula mFormulaText;
Justin Klaassen44595162015-05-28 17:55:20 -0700224 private CalculatorResult mResultText;
Annie Chine918fd22016-03-09 11:07:54 -0800225 private HorizontalScrollView mFormulaContainer;
Annie Chin09547532016-10-14 10:59:07 -0700226 private DragLayout mDragLayout;
Annie Chind0f87d22016-10-24 09:04:12 -0700227 private FrameLayout mHistoryFrame;
Justin Klaassend48b7562015-04-16 16:51:38 -0700228
Justin Klaassen3b4d13d2014-06-06 18:18:37 +0100229 private ViewPager mPadViewPager;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700230 private View mDeleteButton;
231 private View mClearButton;
Justin Klaassend48b7562015-04-16 16:51:38 -0700232 private View mEqualButton;
Justin Klaassene2711cb2015-05-28 11:13:17 -0700233
234 private TextView mInverseToggle;
235 private TextView mModeToggle;
236
Justin Klaassen721ec842015-05-28 14:30:08 -0700237 private View[] mInvertibleButtons;
Justin Klaassene2711cb2015-05-28 11:13:17 -0700238 private View[] mInverseButtons;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700239
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700240 private View mCurrentButton;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700241 private Animator mCurrentAnimator;
242
Hans Boehm8a4f81c2015-07-09 10:41:25 -0700243 // Characters that were recently entered at the end of the display that have not yet
244 // been added to the underlying expression.
245 private String mUnprocessedChars = null;
246
247 // Color to highlight unprocessed characters from physical keyboard.
248 // TODO: should probably match this to the error color?
249 private ForegroundColorSpan mUnprocessedColorSpan = new ForegroundColorSpan(Color.RED);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700250
Annie Chin26e159e2016-05-18 15:17:14 -0700251 // Whether the display is one line.
252 private boolean mOneLine;
253
Annie Chin09547532016-10-14 10:59:07 -0700254 private HistoryFragment mHistoryFragment = new HistoryFragment();
255
Justin Klaassen4b3af052014-05-27 17:53:10 -0700256 @Override
257 protected void onCreate(Bundle savedInstanceState) {
258 super.onCreate(savedInstanceState);
Annie Chin09547532016-10-14 10:59:07 -0700259 setContentView(R.layout.activity_calculator_main);
Justin Klaassend48b7562015-04-16 16:51:38 -0700260 setActionBar((Toolbar) findViewById(R.id.toolbar));
261
262 // Hide all default options in the ActionBar.
263 getActionBar().setDisplayOptions(0);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700264
Justin Klaassen9d33cdc2016-02-21 14:16:14 -0800265 // Ensure the toolbar stays visible while the options menu is displayed.
266 getActionBar().addOnMenuVisibilityListener(new ActionBar.OnMenuVisibilityListener() {
267 @Override
268 public void onMenuVisibilityChanged(boolean isVisible) {
269 mDisplayView.setForceToolbarVisible(isVisible);
270 }
271 });
272
273 mDisplayView = (CalculatorDisplay) findViewById(R.id.display);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700274 mModeView = (TextView) findViewById(R.id.mode);
Christine Franks7452d3a2016-10-27 13:41:18 -0700275 mFormulaText = (CalculatorFormula) findViewById(R.id.formula);
Justin Klaassen44595162015-05-28 17:55:20 -0700276 mResultText = (CalculatorResult) findViewById(R.id.result);
Annie Chine918fd22016-03-09 11:07:54 -0800277 mFormulaContainer = (HorizontalScrollView) findViewById(R.id.formula_container);
Justin Klaassend48b7562015-04-16 16:51:38 -0700278
Justin Klaassen3b4d13d2014-06-06 18:18:37 +0100279 mPadViewPager = (ViewPager) findViewById(R.id.pad_pager);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700280 mDeleteButton = findViewById(R.id.del);
281 mClearButton = findViewById(R.id.clr);
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700282 mEqualButton = findViewById(R.id.pad_numeric).findViewById(R.id.eq);
283 if (mEqualButton == null || mEqualButton.getVisibility() != View.VISIBLE) {
284 mEqualButton = findViewById(R.id.pad_operator).findViewById(R.id.eq);
285 }
Justin Klaassene2711cb2015-05-28 11:13:17 -0700286
287 mInverseToggle = (TextView) findViewById(R.id.toggle_inv);
288 mModeToggle = (TextView) findViewById(R.id.toggle_mode);
289
Annie Chin26e159e2016-05-18 15:17:14 -0700290 mOneLine = mResultText.getVisibility() == View.INVISIBLE;
291
Justin Klaassen721ec842015-05-28 14:30:08 -0700292 mInvertibleButtons = new View[] {
293 findViewById(R.id.fun_sin),
294 findViewById(R.id.fun_cos),
Hans Boehm4db31b42015-05-31 12:19:05 -0700295 findViewById(R.id.fun_tan),
296 findViewById(R.id.fun_ln),
297 findViewById(R.id.fun_log),
298 findViewById(R.id.op_sqrt)
Justin Klaassene2711cb2015-05-28 11:13:17 -0700299 };
300 mInverseButtons = new View[] {
301 findViewById(R.id.fun_arcsin),
302 findViewById(R.id.fun_arccos),
Hans Boehm4db31b42015-05-31 12:19:05 -0700303 findViewById(R.id.fun_arctan),
304 findViewById(R.id.fun_exp),
305 findViewById(R.id.fun_10pow),
306 findViewById(R.id.op_sqr)
Justin Klaassene2711cb2015-05-28 11:13:17 -0700307 };
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700308
Annie Chin06fd3cf2016-11-07 16:04:33 -0800309 mEvaluator = Evaluator.getInstance(this);
Hans Boehm8f051c32016-10-03 16:53:58 -0700310 mResultText.setEvaluator(mEvaluator, Evaluator.MAIN_INDEX);
Hans Boehm013969e2015-04-13 20:29:47 -0700311 KeyMaps.setActivity(this);
Justin Klaassen2be4fdb2014-08-06 19:54:09 -0700312
Annie Chin09547532016-10-14 10:59:07 -0700313 mDragLayout = (DragLayout) findViewById(R.id.drag_layout);
Annie Chind0f87d22016-10-24 09:04:12 -0700314 mDragLayout.removeDragCallback(mDragCallback);
315 mDragLayout.addDragCallback(mDragCallback);
Annie Chin09547532016-10-14 10:59:07 -0700316
Annie Chind0f87d22016-10-24 09:04:12 -0700317 mHistoryFrame = (FrameLayout) findViewById(R.id.history_frame);
Annie Chin09547532016-10-14 10:59:07 -0700318
Hans Boehm84614952014-11-25 18:46:17 -0800319 if (savedInstanceState != null) {
Annie Chinbc001882016-11-09 19:41:21 -0800320 final CalculatorState savedState = CalculatorState.values()[
321 savedInstanceState.getInt(KEY_DISPLAY_STATE,
322 CalculatorState.INPUT.ordinal())];
323 setState(savedState);
Hans Boehm760a9dc2015-04-20 10:27:12 -0700324 CharSequence unprocessed = savedInstanceState.getCharSequence(KEY_UNPROCESSED_CHARS);
325 if (unprocessed != null) {
326 mUnprocessedChars = unprocessed.toString();
327 }
328 byte[] state = savedInstanceState.getByteArray(KEY_EVAL_STATE);
Hans Boehm84614952014-11-25 18:46:17 -0800329 if (state != null) {
330 try (ObjectInput in = new ObjectInputStream(new ByteArrayInputStream(state))) {
331 mEvaluator.restoreInstanceState(in);
332 } catch (Throwable ignored) {
333 // When in doubt, revert to clean state
334 mCurrentState = CalculatorState.INPUT;
Hans Boehm8f051c32016-10-03 16:53:58 -0700335 mEvaluator.clearMain();
Hans Boehm84614952014-11-25 18:46:17 -0800336 }
337 }
Hans Boehmfbcef702015-04-27 18:07:47 -0700338 } else {
339 mCurrentState = CalculatorState.INPUT;
Hans Boehm8f051c32016-10-03 16:53:58 -0700340 mEvaluator.clearMain();
Hans Boehm84614952014-11-25 18:46:17 -0800341 }
Justin Klaassene2711cb2015-05-28 11:13:17 -0700342
Hans Boehm08e8f322015-04-21 13:18:38 -0700343 mFormulaText.setOnTextSizeChangeListener(this);
Justin Klaassenfc5ac822015-06-18 13:15:17 -0700344 mFormulaText.setOnPasteListener(this);
Annie Chine918fd22016-03-09 11:07:54 -0800345 mFormulaText.addTextChangedListener(mFormulaTextWatcher);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700346 mDeleteButton.setOnLongClickListener(this);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700347
Justin Klaassen3e223ea2016-02-05 14:18:06 -0800348 onInverseToggled(savedInstanceState != null
349 && savedInstanceState.getBoolean(KEY_INVERSE_MODE));
Christine Frankseeff27f2016-07-29 12:05:29 -0700350
Hans Boehm8f051c32016-10-03 16:53:58 -0700351 onModeChanged(mEvaluator.getDegreeMode(Evaluator.MAIN_INDEX));
Christine Frankseeff27f2016-07-29 12:05:29 -0700352 if (savedInstanceState != null &&
353 savedInstanceState.getBoolean(KEY_SHOW_TOOLBAR, true) == false) {
354 mDisplayView.hideToolbar();
355 } else {
356 showAndMaybeHideToolbar();
357 }
Justin Klaassene2711cb2015-05-28 11:13:17 -0700358
Hans Boehm84614952014-11-25 18:46:17 -0800359 if (mCurrentState != CalculatorState.INPUT) {
Hans Boehmfbcef702015-04-27 18:07:47 -0700360 // Just reevaluate.
361 redisplayFormula();
Hans Boehm84614952014-11-25 18:46:17 -0800362 setState(CalculatorState.INIT);
Hans Boehmd4959e82016-11-15 18:01:28 -0800363 // Request evaluation when we know display width.
364 mResultText.setShouldRequireResult(true, this);
Hans Boehm84614952014-11-25 18:46:17 -0800365 } else {
Hans Boehmd4959e82016-11-15 18:01:28 -0800366 // This resultText will explicitly call evaluateAndNotify when ready.
367 mResultText.setShouldRequireResult(false, null);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700368 redisplayAfterFormulaChange();
Hans Boehm84614952014-11-25 18:46:17 -0800369 }
370 // TODO: We're currently not saving and restoring scroll position.
371 // We probably should. Details may require care to deal with:
372 // - new display size
373 // - slow recomputation if we've scrolled far.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700374 }
375
376 @Override
Justin Klaassen9d33cdc2016-02-21 14:16:14 -0800377 protected void onResume() {
378 super.onResume();
Christine Frankseeff27f2016-07-29 12:05:29 -0700379 if (mDisplayView.isToolbarVisible()) {
380 showAndMaybeHideToolbar();
381 }
Justin Klaassen9d33cdc2016-02-21 14:16:14 -0800382 }
383
384 @Override
Justin Klaassenf79d6f62014-08-26 12:27:08 -0700385 protected void onSaveInstanceState(@NonNull Bundle outState) {
Hans Boehm40125442016-01-22 10:35:35 -0800386 mEvaluator.cancelAll(true);
Justin Klaassenf79d6f62014-08-26 12:27:08 -0700387 // If there's an animation in progress, cancel it first to ensure our state is up-to-date.
388 if (mCurrentAnimator != null) {
389 mCurrentAnimator.cancel();
390 }
391
Justin Klaassen4b3af052014-05-27 17:53:10 -0700392 super.onSaveInstanceState(outState);
Hans Boehm84614952014-11-25 18:46:17 -0800393 outState.putInt(KEY_DISPLAY_STATE, mCurrentState.ordinal());
Hans Boehm760a9dc2015-04-20 10:27:12 -0700394 outState.putCharSequence(KEY_UNPROCESSED_CHARS, mUnprocessedChars);
Hans Boehm84614952014-11-25 18:46:17 -0800395 ByteArrayOutputStream byteArrayStream = new ByteArrayOutputStream();
396 try (ObjectOutput out = new ObjectOutputStream(byteArrayStream)) {
397 mEvaluator.saveInstanceState(out);
398 } catch (IOException e) {
399 // Impossible; No IO involved.
400 throw new AssertionError("Impossible IO exception", e);
401 }
402 outState.putByteArray(KEY_EVAL_STATE, byteArrayStream.toByteArray());
Justin Klaassen3e223ea2016-02-05 14:18:06 -0800403 outState.putBoolean(KEY_INVERSE_MODE, mInverseToggle.isSelected());
Christine Frankseeff27f2016-07-29 12:05:29 -0700404 outState.putBoolean(KEY_SHOW_TOOLBAR, mDisplayView.isToolbarVisible());
Justin Klaassen4b3af052014-05-27 17:53:10 -0700405 }
406
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700407 // Set the state, updating delete label and display colors.
408 // This restores display positions on moving to INPUT.
Justin Klaassend48b7562015-04-16 16:51:38 -0700409 // But movement/animation for moving to RESULT has already been done.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700410 private void setState(CalculatorState state) {
411 if (mCurrentState != state) {
Hans Boehm84614952014-11-25 18:46:17 -0800412 if (state == CalculatorState.INPUT) {
Hans Boehmd4959e82016-11-15 18:01:28 -0800413 // We'll explicitly request evaluation from now on.
414 mResultText.setShouldRequireResult(false, null);
Hans Boehm84614952014-11-25 18:46:17 -0800415 restoreDisplayPositions();
416 }
Justin Klaassen4b3af052014-05-27 17:53:10 -0700417 mCurrentState = state;
418
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700419 if (mCurrentState == CalculatorState.RESULT) {
420 // No longer do this for ERROR; allow mistakes to be corrected.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700421 mDeleteButton.setVisibility(View.GONE);
422 mClearButton.setVisibility(View.VISIBLE);
423 } else {
424 mDeleteButton.setVisibility(View.VISIBLE);
425 mClearButton.setVisibility(View.GONE);
426 }
427
Annie Chin26e159e2016-05-18 15:17:14 -0700428 if (mOneLine) {
429 if (mCurrentState == CalculatorState.RESULT
430 || mCurrentState == CalculatorState.EVALUATE
431 || mCurrentState == CalculatorState.ANIMATE) {
432 mFormulaText.setVisibility(View.VISIBLE);
433 mResultText.setVisibility(View.VISIBLE);
Annie Chin947d93b2016-06-14 10:18:54 -0700434 } else if (mCurrentState == CalculatorState.ERROR) {
435 mFormulaText.setVisibility(View.INVISIBLE);
436 mResultText.setVisibility(View.VISIBLE);
Annie Chin26e159e2016-05-18 15:17:14 -0700437 } else {
438 mFormulaText.setVisibility(View.VISIBLE);
439 mResultText.setVisibility(View.INVISIBLE);
440 }
441 }
442
Hans Boehm84614952014-11-25 18:46:17 -0800443 if (mCurrentState == CalculatorState.ERROR) {
Chenjie Yu3937b652016-06-01 23:14:26 -0700444 final int errorColor =
445 ContextCompat.getColor(this, R.color.calculator_error_color);
Hans Boehm08e8f322015-04-21 13:18:38 -0700446 mFormulaText.setTextColor(errorColor);
Justin Klaassen44595162015-05-28 17:55:20 -0700447 mResultText.setTextColor(errorColor);
Justin Klaassen8fff1442014-06-19 10:43:29 -0700448 getWindow().setStatusBarColor(errorColor);
Justin Klaassen44595162015-05-28 17:55:20 -0700449 } else if (mCurrentState != CalculatorState.RESULT) {
Chenjie Yu3937b652016-06-01 23:14:26 -0700450 mFormulaText.setTextColor(
451 ContextCompat.getColor(this, R.color.display_formula_text_color));
452 mResultText.setTextColor(
453 ContextCompat.getColor(this, R.color.display_result_text_color));
454 getWindow().setStatusBarColor(
455 ContextCompat.getColor(this, R.color.calculator_accent_color));
Justin Klaassen4b3af052014-05-27 17:53:10 -0700456 }
Justin Klaassend48b7562015-04-16 16:51:38 -0700457
458 invalidateOptionsMenu();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700459 }
460 }
461
Annie Chin70ac8ea2016-11-18 14:43:56 -0800462 public boolean isResultState() {
463 return mCurrentState == CalculatorState.RESULT;
464 }
465
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700466 @Override
Annie Chind0f87d22016-10-24 09:04:12 -0700467 protected void onDestroy() {
468 mDragLayout.removeDragCallback(mDragCallback);
469 super.onDestroy();
470 }
471
472 @Override
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700473 public void onActionModeStarted(ActionMode mode) {
474 super.onActionModeStarted(mode);
Christine Franks7452d3a2016-10-27 13:41:18 -0700475 if (mode.getTag() == CalculatorFormula.TAG_ACTION_MODE) {
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700476 mFormulaContainer.scrollTo(mFormulaText.getRight(), 0);
477 }
478 }
479
Chenjie Yu3937b652016-06-01 23:14:26 -0700480 /**
481 * Stop any active ActionMode or ContextMenu for copy/paste actions.
482 * Return true if there was one.
483 */
484 private boolean stopActionModeOrContextMenu() {
485 if (mResultText.stopActionModeOrContextMenu()) {
Hans Boehm1176f232015-05-11 16:26:03 -0700486 return true;
487 }
Chenjie Yu3937b652016-06-01 23:14:26 -0700488 if (mFormulaText.stopActionModeOrContextMenu()) {
Hans Boehm1176f232015-05-11 16:26:03 -0700489 return true;
490 }
491 return false;
492 }
493
Justin Klaassen4b3af052014-05-27 17:53:10 -0700494 @Override
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700495 public void onUserInteraction() {
496 super.onUserInteraction();
497
498 // If there's an animation in progress, end it immediately, so the user interaction can
499 // be handled.
500 if (mCurrentAnimator != null) {
501 mCurrentAnimator.end();
502 }
503 }
504
505 @Override
Justin Klaassen3b4d13d2014-06-06 18:18:37 +0100506 public void onBackPressed() {
Chenjie Yu3937b652016-06-01 23:14:26 -0700507 if (!stopActionModeOrContextMenu()) {
Annie Chin09547532016-10-14 10:59:07 -0700508 if (mDragLayout.isOpen()) {
Annie Chin09547532016-10-14 10:59:07 -0700509 mDragLayout.setClosed();
Annie Chin06fd3cf2016-11-07 16:04:33 -0800510 popFragmentBackstack();
Annie Chin09547532016-10-14 10:59:07 -0700511 return;
512 }
Hans Boehm1176f232015-05-11 16:26:03 -0700513 if (mPadViewPager != null && mPadViewPager.getCurrentItem() != 0) {
514 // Select the previous pad.
515 mPadViewPager.setCurrentItem(mPadViewPager.getCurrentItem() - 1);
516 } else {
517 // If the user is currently looking at the first pad (or the pad is not paged),
518 // allow the system to handle the Back button.
519 super.onBackPressed();
520 }
Justin Klaassen3b4d13d2014-06-06 18:18:37 +0100521 }
522 }
523
524 @Override
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700525 public boolean onKeyUp(int keyCode, KeyEvent event) {
Justin Klaassen83959da2016-04-06 11:55:24 -0700526 // Allow the system to handle special key codes (e.g. "BACK" or "DPAD").
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700527 switch (keyCode) {
Justin Klaassen83959da2016-04-06 11:55:24 -0700528 case KeyEvent.KEYCODE_BACK:
Christine Franksf9ba2202016-10-20 17:20:19 -0700529 case KeyEvent.KEYCODE_ESCAPE:
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700530 case KeyEvent.KEYCODE_DPAD_UP:
531 case KeyEvent.KEYCODE_DPAD_DOWN:
532 case KeyEvent.KEYCODE_DPAD_LEFT:
533 case KeyEvent.KEYCODE_DPAD_RIGHT:
534 return super.onKeyUp(keyCode, event);
535 }
536
Chenjie Yu3937b652016-06-01 23:14:26 -0700537 // Stop the action mode or context menu if it's showing.
538 stopActionModeOrContextMenu();
Justin Klaassend12e0622016-04-27 16:26:47 -0700539
Hans Boehmced295e2016-11-17 17:30:13 -0800540 // Always cancel unrequested in-progress evaluation of the main expression, so that
541 // we don't have to worry about subsequent asynchronous completion.
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700542 // Requested in-progress evaluations are handled below.
543 if (mCurrentState != CalculatorState.EVALUATE) {
Hans Boehmced295e2016-11-17 17:30:13 -0800544 mEvaluator.cancel(Evaluator.MAIN_INDEX, true);
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700545 }
546
547 switch (keyCode) {
548 case KeyEvent.KEYCODE_NUMPAD_ENTER:
549 case KeyEvent.KEYCODE_ENTER:
550 case KeyEvent.KEYCODE_DPAD_CENTER:
551 mCurrentButton = mEqualButton;
552 onEquals();
553 return true;
554 case KeyEvent.KEYCODE_DEL:
555 mCurrentButton = mDeleteButton;
556 onDelete();
557 return true;
Annie Chin56bcbf12016-09-23 17:04:22 -0700558 case KeyEvent.KEYCODE_CLEAR:
559 mCurrentButton = mClearButton;
560 onClear();
561 return true;
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700562 default:
563 cancelIfEvaluating(false);
564 final int raw = event.getKeyCharacterMap().get(keyCode, event.getMetaState());
565 if ((raw & KeyCharacterMap.COMBINING_ACCENT) != 0) {
566 return true; // discard
567 }
568 // Try to discard non-printing characters and the like.
569 // The user will have to explicitly delete other junk that gets past us.
570 if (Character.isIdentifierIgnorable(raw) || Character.isWhitespace(raw)) {
571 return true;
572 }
573 char c = (char) raw;
574 if (c == '=') {
575 mCurrentButton = mEqualButton;
576 onEquals();
577 } else {
578 addChars(String.valueOf(c), true);
579 redisplayAfterFormulaChange();
580 }
581 return true;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700582 }
583 }
584
Justin Klaassene2711cb2015-05-28 11:13:17 -0700585 /**
586 * Invoked whenever the inverse button is toggled to update the UI.
587 *
588 * @param showInverse {@code true} if inverse functions should be shown
589 */
590 private void onInverseToggled(boolean showInverse) {
Justin Klaassen3e223ea2016-02-05 14:18:06 -0800591 mInverseToggle.setSelected(showInverse);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700592 if (showInverse) {
593 mInverseToggle.setContentDescription(getString(R.string.desc_inv_on));
Justin Klaassen721ec842015-05-28 14:30:08 -0700594 for (View invertibleButton : mInvertibleButtons) {
595 invertibleButton.setVisibility(View.GONE);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700596 }
597 for (View inverseButton : mInverseButtons) {
598 inverseButton.setVisibility(View.VISIBLE);
599 }
600 } else {
601 mInverseToggle.setContentDescription(getString(R.string.desc_inv_off));
Justin Klaassen721ec842015-05-28 14:30:08 -0700602 for (View invertibleButton : mInvertibleButtons) {
603 invertibleButton.setVisibility(View.VISIBLE);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700604 }
605 for (View inverseButton : mInverseButtons) {
606 inverseButton.setVisibility(View.GONE);
607 }
608 }
609 }
610
611 /**
Christine Frankseeff27f2016-07-29 12:05:29 -0700612 * Invoked whenever the deg/rad mode may have changed to update the UI. Note that the mode has
613 * not necessarily actually changed where this is invoked.
Justin Klaassene2711cb2015-05-28 11:13:17 -0700614 *
615 * @param degreeMode {@code true} if in degree mode
616 */
617 private void onModeChanged(boolean degreeMode) {
618 if (degreeMode) {
Justin Klaassend48b7562015-04-16 16:51:38 -0700619 mModeView.setText(R.string.mode_deg);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700620 mModeView.setContentDescription(getString(R.string.desc_mode_deg));
621
622 mModeToggle.setText(R.string.mode_rad);
623 mModeToggle.setContentDescription(getString(R.string.desc_switch_rad));
Hans Boehmbfe8c222015-04-02 16:26:07 -0700624 } else {
Justin Klaassend48b7562015-04-16 16:51:38 -0700625 mModeView.setText(R.string.mode_rad);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700626 mModeView.setContentDescription(getString(R.string.desc_mode_rad));
627
628 mModeToggle.setText(R.string.mode_deg);
629 mModeToggle.setContentDescription(getString(R.string.desc_switch_deg));
Hans Boehmbfe8c222015-04-02 16:26:07 -0700630 }
631 }
Hans Boehm84614952014-11-25 18:46:17 -0800632
Annie Chin06fd3cf2016-11-07 16:04:33 -0800633 private void popFragmentBackstack() {
634 final FragmentManager manager = getFragmentManager();
635 if (manager == null || manager.isDestroyed()) {
636 return;
637 }
638 manager.popBackStack();
639 }
Hans Boehm5d79d102015-09-16 16:33:47 -0700640 /**
641 * Switch to INPUT from RESULT state in response to input of the specified button_id.
642 * View.NO_ID is treated as an incomplete function id.
643 */
644 private void switchToInput(int button_id) {
645 if (KeyMaps.isBinary(button_id) || KeyMaps.isSuffix(button_id)) {
Hans Boehm8f051c32016-10-03 16:53:58 -0700646 mEvaluator.collapse(mEvaluator.getMaxIndex() /* Most recent history entry */);
Hans Boehm5d79d102015-09-16 16:33:47 -0700647 } else {
648 announceClearedForAccessibility();
Hans Boehm8f051c32016-10-03 16:53:58 -0700649 mEvaluator.clearMain();
Hans Boehm5d79d102015-09-16 16:33:47 -0700650 }
651 setState(CalculatorState.INPUT);
652 }
653
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700654 // Add the given button id to input expression.
655 // If appropriate, clear the expression before doing so.
656 private void addKeyToExpr(int id) {
657 if (mCurrentState == CalculatorState.ERROR) {
658 setState(CalculatorState.INPUT);
659 } else if (mCurrentState == CalculatorState.RESULT) {
Hans Boehm5d79d102015-09-16 16:33:47 -0700660 switchToInput(id);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700661 }
662 if (!mEvaluator.append(id)) {
663 // TODO: Some user visible feedback?
664 }
665 }
666
Hans Boehm017de982015-06-10 17:46:03 -0700667 /**
668 * Add the given button id to input expression, assuming it was explicitly
669 * typed/touched.
670 * We perform slightly more aggressive correction than in pasted expressions.
671 */
672 private void addExplicitKeyToExpr(int id) {
673 if (mCurrentState == CalculatorState.INPUT && id == R.id.op_sub) {
Hans Boehm8f051c32016-10-03 16:53:58 -0700674 mEvaluator.getExpr(Evaluator.MAIN_INDEX).removeTrailingAdditiveOperators();
Hans Boehm017de982015-06-10 17:46:03 -0700675 }
676 addKeyToExpr(id);
677 }
678
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700679 private void redisplayAfterFormulaChange() {
680 // TODO: Could do this more incrementally.
681 redisplayFormula();
682 setState(CalculatorState.INPUT);
Hans Boehm8f051c32016-10-03 16:53:58 -0700683 mResultText.clear();
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800684 if (haveUnprocessed()) {
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800685 // Force reevaluation when text is deleted, even if expression is unchanged.
686 mEvaluator.touch();
687 } else {
Hans Boehm8f051c32016-10-03 16:53:58 -0700688 if (mEvaluator.getExpr(Evaluator.MAIN_INDEX).hasInterestingOps()) {
689 mEvaluator.evaluateAndNotify(Evaluator.MAIN_INDEX, this, mResultText);
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800690 }
Hans Boehmc023b732015-04-29 11:30:47 -0700691 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700692 }
693
Hans Boehm52d477a2016-04-01 17:42:50 -0700694 /**
695 * Show the toolbar.
696 * Automatically hide it again if it's not relevant to current formula.
697 */
698 private void showAndMaybeHideToolbar() {
699 final boolean shouldBeVisible =
700 mCurrentState == CalculatorState.INPUT && mEvaluator.hasTrigFuncs();
701 mDisplayView.showToolbar(!shouldBeVisible);
702 }
703
704 /**
705 * Display or hide the toolbar depending on calculator state.
706 */
707 private void showOrHideToolbar() {
708 final boolean shouldBeVisible =
709 mCurrentState == CalculatorState.INPUT && mEvaluator.hasTrigFuncs();
710 if (shouldBeVisible) {
711 mDisplayView.showToolbar(false);
712 } else {
713 mDisplayView.hideToolbar();
714 }
715 }
716
Justin Klaassen4b3af052014-05-27 17:53:10 -0700717 public void onButtonClick(View view) {
Hans Boehmc1ea0912015-06-19 15:05:07 -0700718 // Any animation is ended before we get here.
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700719 mCurrentButton = view;
Chenjie Yu3937b652016-06-01 23:14:26 -0700720 stopActionModeOrContextMenu();
Justin Klaassen9d33cdc2016-02-21 14:16:14 -0800721
Hans Boehmc1ea0912015-06-19 15:05:07 -0700722 // See onKey above for the rationale behind some of the behavior below:
723 if (mCurrentState != CalculatorState.EVALUATE) {
Hans Boehmced295e2016-11-17 17:30:13 -0800724 // Cancel main expression evaluations that were not specifically requested.
725 mEvaluator.cancel(Evaluator.MAIN_INDEX, true);
Hans Boehm84614952014-11-25 18:46:17 -0800726 }
Justin Klaassen9d33cdc2016-02-21 14:16:14 -0800727
Justin Klaassend48b7562015-04-16 16:51:38 -0700728 final int id = view.getId();
Hans Boehm84614952014-11-25 18:46:17 -0800729 switch (id) {
Justin Klaassen4b3af052014-05-27 17:53:10 -0700730 case R.id.eq:
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700731 onEquals();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700732 break;
733 case R.id.del:
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700734 onDelete();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700735 break;
736 case R.id.clr:
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700737 onClear();
Hans Boehm52d477a2016-04-01 17:42:50 -0700738 return; // Toolbar visibility adjusted at end of animation.
Justin Klaassene2711cb2015-05-28 11:13:17 -0700739 case R.id.toggle_inv:
740 final boolean selected = !mInverseToggle.isSelected();
741 mInverseToggle.setSelected(selected);
742 onInverseToggled(selected);
Hans Boehmc1ea0912015-06-19 15:05:07 -0700743 if (mCurrentState == CalculatorState.RESULT) {
744 mResultText.redisplay(); // In case we cancelled reevaluation.
745 }
Justin Klaassene2711cb2015-05-28 11:13:17 -0700746 break;
747 case R.id.toggle_mode:
Hans Boehmc1ea0912015-06-19 15:05:07 -0700748 cancelIfEvaluating(false);
Hans Boehm8f051c32016-10-03 16:53:58 -0700749 final boolean mode = !mEvaluator.getDegreeMode(Evaluator.MAIN_INDEX);
750 if (mCurrentState == CalculatorState.RESULT
751 && mEvaluator.getExpr(Evaluator.MAIN_INDEX).hasTrigFuncs()) {
752 // Capture current result evaluated in old mode.
753 mEvaluator.collapse(mEvaluator.getMaxIndex());
Hans Boehmbfe8c222015-04-02 16:26:07 -0700754 redisplayFormula();
755 }
756 // In input mode, we reinterpret already entered trig functions.
757 mEvaluator.setDegreeMode(mode);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700758 onModeChanged(mode);
Christine Frankseeff27f2016-07-29 12:05:29 -0700759 // Show the toolbar to highlight the mode change.
760 showAndMaybeHideToolbar();
Hans Boehmbfe8c222015-04-02 16:26:07 -0700761 setState(CalculatorState.INPUT);
Justin Klaassen44595162015-05-28 17:55:20 -0700762 mResultText.clear();
Hans Boehm8f051c32016-10-03 16:53:58 -0700763 if (!haveUnprocessed()
764 && mEvaluator.getExpr(Evaluator.MAIN_INDEX).hasInterestingOps()) {
765 mEvaluator.evaluateAndNotify(mEvaluator.MAIN_INDEX, this, mResultText);
Hans Boehmc023b732015-04-29 11:30:47 -0700766 }
Christine Frankseeff27f2016-07-29 12:05:29 -0700767 return;
Hans Boehm8f051c32016-10-03 16:53:58 -0700768 case R.id.memory_store:
Annie Chin37c33b62016-11-22 14:46:28 -0800769 mResultText.onMemoryStore();
Hans Boehm8f051c32016-10-03 16:53:58 -0700770 return;
771 case R.id.memory_recall:
772 onMemoryRecall();
773 return;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700774 default:
Hans Boehmc1ea0912015-06-19 15:05:07 -0700775 cancelIfEvaluating(false);
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800776 if (haveUnprocessed()) {
777 // For consistency, append as uninterpreted characters.
778 // This may actually be useful for a left parenthesis.
779 addChars(KeyMaps.toString(this, id), true);
780 } else {
781 addExplicitKeyToExpr(id);
782 redisplayAfterFormulaChange();
783 }
Justin Klaassen4b3af052014-05-27 17:53:10 -0700784 break;
785 }
Hans Boehm52d477a2016-04-01 17:42:50 -0700786 showOrHideToolbar();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700787 }
788
Hans Boehm84614952014-11-25 18:46:17 -0800789 void redisplayFormula() {
Hans Boehm8f051c32016-10-03 16:53:58 -0700790 SpannableStringBuilder formula
791 = mEvaluator.getExpr(Evaluator.MAIN_INDEX).toSpannableStringBuilder(this);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700792 if (mUnprocessedChars != null) {
793 // Add and highlight characters we couldn't process.
Hans Boehm8a4f81c2015-07-09 10:41:25 -0700794 formula.append(mUnprocessedChars, mUnprocessedColorSpan,
795 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700796 }
Hans Boehm8a4f81c2015-07-09 10:41:25 -0700797 mFormulaText.changeTextTo(formula);
Annie Chinf360ef02016-03-10 13:45:39 -0800798 mFormulaText.setContentDescription(TextUtils.isEmpty(formula)
Justin Klaassend1831412016-07-19 21:59:10 -0700799 ? getString(R.string.desc_formula) : null);
Hans Boehm84614952014-11-25 18:46:17 -0800800 }
801
Justin Klaassen4b3af052014-05-27 17:53:10 -0700802 @Override
803 public boolean onLongClick(View view) {
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700804 mCurrentButton = view;
805
Justin Klaassen4b3af052014-05-27 17:53:10 -0700806 if (view.getId() == R.id.del) {
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700807 onClear();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700808 return true;
809 }
810 return false;
811 }
812
Hans Boehm84614952014-11-25 18:46:17 -0800813 // Initial evaluation completed successfully. Initiate display.
Hans Boehm8f051c32016-10-03 16:53:58 -0700814 public void onEvaluate(long index, int initDisplayPrec, int msd, int leastDigPos,
Hans Boehma0e45f32015-05-30 13:20:35 -0700815 String truncatedWholeNumber) {
Hans Boehm8f051c32016-10-03 16:53:58 -0700816 if (index != Evaluator.MAIN_INDEX) {
817 throw new AssertionError("Unexpected evaluation result index\n");
818 }
Annie Chin37c33b62016-11-22 14:46:28 -0800819
Justin Klaassend48b7562015-04-16 16:51:38 -0700820 // Invalidate any options that may depend on the current result.
821 invalidateOptionsMenu();
822
Hans Boehm8f051c32016-10-03 16:53:58 -0700823 mResultText.onEvaluate(index, initDisplayPrec, msd, leastDigPos, truncatedWholeNumber);
Hans Boehm61568a12015-05-18 18:25:41 -0700824 if (mCurrentState != CalculatorState.INPUT) { // in EVALUATE or INIT state
Hans Boehm84614952014-11-25 18:46:17 -0800825 onResult(mCurrentState != CalculatorState.INIT);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700826 }
Hans Boehm84614952014-11-25 18:46:17 -0800827 }
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700828
Hans Boehmc1ea0912015-06-19 15:05:07 -0700829 // Reset state to reflect evaluator cancellation. Invoked by evaluator.
Hans Boehm8f051c32016-10-03 16:53:58 -0700830 public void onCancelled(long index) {
831 // Index is Evaluator.MAIN_INDEX. We should be in EVALUATE state.
Hans Boehm84614952014-11-25 18:46:17 -0800832 setState(CalculatorState.INPUT);
Hans Boehm8f051c32016-10-03 16:53:58 -0700833 mResultText.onCancelled(index);
Hans Boehm84614952014-11-25 18:46:17 -0800834 }
835
836 // Reevaluation completed; ask result to redisplay current value.
Hans Boehm8f051c32016-10-03 16:53:58 -0700837 public void onReevaluate(long index)
Hans Boehm84614952014-11-25 18:46:17 -0800838 {
Hans Boehm8f051c32016-10-03 16:53:58 -0700839 // Index is Evaluator.MAIN_INDEX.
840 mResultText.onReevaluate(index);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700841 }
842
Justin Klaassenfed941a2014-06-09 18:42:40 +0100843 @Override
844 public void onTextSizeChanged(final TextView textView, float oldSize) {
845 if (mCurrentState != CalculatorState.INPUT) {
846 // Only animate text changes that occur from user input.
847 return;
848 }
849
850 // Calculate the values needed to perform the scale and translation animations,
851 // maintaining the same apparent baseline for the displayed text.
852 final float textScale = oldSize / textView.getTextSize();
853 final float translationX = (1.0f - textScale) *
854 (textView.getWidth() / 2.0f - textView.getPaddingEnd());
855 final float translationY = (1.0f - textScale) *
856 (textView.getHeight() / 2.0f - textView.getPaddingBottom());
857
858 final AnimatorSet animatorSet = new AnimatorSet();
859 animatorSet.playTogether(
860 ObjectAnimator.ofFloat(textView, View.SCALE_X, textScale, 1.0f),
861 ObjectAnimator.ofFloat(textView, View.SCALE_Y, textScale, 1.0f),
862 ObjectAnimator.ofFloat(textView, View.TRANSLATION_X, translationX, 0.0f),
863 ObjectAnimator.ofFloat(textView, View.TRANSLATION_Y, translationY, 0.0f));
Justin Klaassen94db7202014-06-11 11:22:31 -0700864 animatorSet.setDuration(getResources().getInteger(android.R.integer.config_mediumAnimTime));
Justin Klaassenfed941a2014-06-09 18:42:40 +0100865 animatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
866 animatorSet.start();
867 }
868
Hans Boehmc1ea0912015-06-19 15:05:07 -0700869 /**
870 * Cancel any in-progress explicitly requested evaluations.
871 * @param quiet suppress pop-up message. Explicit evaluation can change the expression
872 value, and certainly changes the display, so it seems reasonable to warn.
873 * @return true if there was such an evaluation
874 */
875 private boolean cancelIfEvaluating(boolean quiet) {
876 if (mCurrentState == CalculatorState.EVALUATE) {
Hans Boehm8f051c32016-10-03 16:53:58 -0700877 // TODO: Maybe just cancel main expression evaluation?
Hans Boehmc1ea0912015-06-19 15:05:07 -0700878 mEvaluator.cancelAll(quiet);
879 return true;
880 } else {
881 return false;
882 }
883 }
884
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800885 private boolean haveUnprocessed() {
886 return mUnprocessedChars != null && !mUnprocessedChars.isEmpty();
887 }
888
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700889 private void onEquals() {
Hans Boehm56d6e762016-06-06 11:46:29 -0700890 // Ignore if in non-INPUT state, or if there are no operators.
Justin Klaassena8075af2016-07-27 15:24:45 -0700891 if (mCurrentState == CalculatorState.INPUT) {
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800892 if (haveUnprocessed()) {
Justin Klaassena8075af2016-07-27 15:24:45 -0700893 setState(CalculatorState.EVALUATE);
Hans Boehm8f051c32016-10-03 16:53:58 -0700894 onError(Evaluator.MAIN_INDEX, R.string.error_syntax);
895 } else if (mEvaluator.getExpr(Evaluator.MAIN_INDEX).hasInterestingOps()) {
Justin Klaassena8075af2016-07-27 15:24:45 -0700896 setState(CalculatorState.EVALUATE);
Hans Boehm8f051c32016-10-03 16:53:58 -0700897 mEvaluator.requireResult(Evaluator.MAIN_INDEX, this, mResultText);
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800898 }
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700899 }
900 }
901
902 private void onDelete() {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700903 // Delete works like backspace; remove the last character or operator from the expression.
904 // Note that we handle keyboard delete exactly like the delete button. For
905 // example the delete button can be used to delete a character from an incomplete
906 // function name typed on a physical keyboard.
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700907 // This should be impossible in RESULT state.
Hans Boehmc1ea0912015-06-19 15:05:07 -0700908 // If there is an in-progress explicit evaluation, just cancel it and return.
909 if (cancelIfEvaluating(false)) return;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700910 setState(CalculatorState.INPUT);
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800911 if (haveUnprocessed()) {
912 mUnprocessedChars = mUnprocessedChars.substring(0, mUnprocessedChars.length() - 1);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700913 } else {
Hans Boehmc023b732015-04-29 11:30:47 -0700914 mEvaluator.delete();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700915 }
Hans Boehm8f051c32016-10-03 16:53:58 -0700916 if (mEvaluator.getExpr(Evaluator.MAIN_INDEX).isEmpty() && !haveUnprocessed()) {
Hans Boehmdb6f9992015-08-19 12:32:56 -0700917 // Resulting formula won't be announced, since it's empty.
918 announceClearedForAccessibility();
919 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700920 redisplayAfterFormulaChange();
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700921 }
922
Hans Boehm8f051c32016-10-03 16:53:58 -0700923 private void onMemoryRecall() {
924 clearIfNotInputState();
925 long memoryIndex = mEvaluator.getMemoryIndex();
926 if (memoryIndex != 0) {
927 mEvaluator.appendExpr(mEvaluator.getMemoryIndex());
928 redisplayAfterFormulaChange();
929 } // FIXME: Avoid the 0 case, e.g. by graying out button when memory is unavailable.
930 }
931
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700932 private void reveal(View sourceView, int colorRes, AnimatorListener listener) {
Justin Klaassen06360f92014-08-28 11:08:44 -0700933 final ViewGroupOverlay groupOverlay =
934 (ViewGroupOverlay) getWindow().getDecorView().getOverlay();
Justin Klaassen8fff1442014-06-19 10:43:29 -0700935
936 final Rect displayRect = new Rect();
Justin Klaassen06360f92014-08-28 11:08:44 -0700937 mDisplayView.getGlobalVisibleRect(displayRect);
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700938
939 // Make reveal cover the display and status bar.
940 final View revealView = new View(this);
Justin Klaassen8fff1442014-06-19 10:43:29 -0700941 revealView.setBottom(displayRect.bottom);
942 revealView.setLeft(displayRect.left);
943 revealView.setRight(displayRect.right);
Chenjie Yu3937b652016-06-01 23:14:26 -0700944 revealView.setBackgroundColor(ContextCompat.getColor(this, colorRes));
Justin Klaassen06360f92014-08-28 11:08:44 -0700945 groupOverlay.add(revealView);
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700946
Justin Klaassen4b3af052014-05-27 17:53:10 -0700947 final int[] clearLocation = new int[2];
948 sourceView.getLocationInWindow(clearLocation);
949 clearLocation[0] += sourceView.getWidth() / 2;
950 clearLocation[1] += sourceView.getHeight() / 2;
951
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700952 final int revealCenterX = clearLocation[0] - revealView.getLeft();
953 final int revealCenterY = clearLocation[1] - revealView.getTop();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700954
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700955 final double x1_2 = Math.pow(revealView.getLeft() - revealCenterX, 2);
956 final double x2_2 = Math.pow(revealView.getRight() - revealCenterX, 2);
957 final double y_2 = Math.pow(revealView.getTop() - revealCenterY, 2);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700958 final float revealRadius = (float) Math.max(Math.sqrt(x1_2 + y_2), Math.sqrt(x2_2 + y_2));
959
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700960 final Animator revealAnimator =
961 ViewAnimationUtils.createCircularReveal(revealView,
ztenghui3d6ecaf2014-06-05 09:56:00 -0700962 revealCenterX, revealCenterY, 0.0f, revealRadius);
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700963 revealAnimator.setDuration(
Justin Klaassen4b3af052014-05-27 17:53:10 -0700964 getResources().getInteger(android.R.integer.config_longAnimTime));
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700965 revealAnimator.addListener(listener);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700966
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700967 final Animator alphaAnimator = ObjectAnimator.ofFloat(revealView, View.ALPHA, 0.0f);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700968 alphaAnimator.setDuration(
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700969 getResources().getInteger(android.R.integer.config_mediumAnimTime));
Justin Klaassen4b3af052014-05-27 17:53:10 -0700970
971 final AnimatorSet animatorSet = new AnimatorSet();
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700972 animatorSet.play(revealAnimator).before(alphaAnimator);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700973 animatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
974 animatorSet.addListener(new AnimatorListenerAdapter() {
975 @Override
Justin Klaassen4b3af052014-05-27 17:53:10 -0700976 public void onAnimationEnd(Animator animator) {
Justin Klaassen8fff1442014-06-19 10:43:29 -0700977 groupOverlay.remove(revealView);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700978 mCurrentAnimator = null;
979 }
980 });
981
982 mCurrentAnimator = animatorSet;
983 animatorSet.start();
984 }
985
Hans Boehmdb6f9992015-08-19 12:32:56 -0700986 private void announceClearedForAccessibility() {
987 mResultText.announceForAccessibility(getResources().getString(R.string.cleared));
Hans Boehmccc55662015-07-07 14:16:59 -0700988 }
989
Hans Boehm9db3ee22016-11-18 10:09:47 -0800990 public void onClearAnimationEnd() {
991 mUnprocessedChars = null;
992 mResultText.clear();
993 mEvaluator.clearMain();
994 setState(CalculatorState.INPUT);
995 redisplayFormula();
996 }
997
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700998 private void onClear() {
Hans Boehm8f051c32016-10-03 16:53:58 -0700999 if (mEvaluator.getExpr(Evaluator.MAIN_INDEX).isEmpty() && !haveUnprocessed()) {
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001000 return;
1001 }
Hans Boehmc1ea0912015-06-19 15:05:07 -07001002 cancelIfEvaluating(true);
Hans Boehmdb6f9992015-08-19 12:32:56 -07001003 announceClearedForAccessibility();
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001004 reveal(mCurrentButton, R.color.calculator_accent_color, new AnimatorListenerAdapter() {
1005 @Override
1006 public void onAnimationEnd(Animator animation) {
Hans Boehm9db3ee22016-11-18 10:09:47 -08001007 onClearAnimationEnd();
Hans Boehm52d477a2016-04-01 17:42:50 -07001008 showOrHideToolbar();
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001009 }
1010 });
1011 }
1012
Hans Boehm84614952014-11-25 18:46:17 -08001013 // Evaluation encountered en error. Display the error.
Hans Boehm8f051c32016-10-03 16:53:58 -07001014 @Override
1015 public void onError(final long index, final int errorResourceId) {
1016 if (index != Evaluator.MAIN_INDEX) {
1017 throw new AssertionError("Unexpected error source");
1018 }
Hans Boehmfbcef702015-04-27 18:07:47 -07001019 if (mCurrentState == CalculatorState.EVALUATE) {
1020 setState(CalculatorState.ANIMATE);
Hans Boehmccc55662015-07-07 14:16:59 -07001021 mResultText.announceForAccessibility(getResources().getString(errorResourceId));
Hans Boehmfbcef702015-04-27 18:07:47 -07001022 reveal(mCurrentButton, R.color.calculator_error_color,
1023 new AnimatorListenerAdapter() {
1024 @Override
1025 public void onAnimationEnd(Animator animation) {
1026 setState(CalculatorState.ERROR);
Hans Boehm8f051c32016-10-03 16:53:58 -07001027 mResultText.onError(index, errorResourceId);
Hans Boehmfbcef702015-04-27 18:07:47 -07001028 }
1029 });
1030 } else if (mCurrentState == CalculatorState.INIT) {
1031 setState(CalculatorState.ERROR);
Hans Boehm8f051c32016-10-03 16:53:58 -07001032 mResultText.onError(index, errorResourceId);
Hans Boehmc023b732015-04-29 11:30:47 -07001033 } else {
Justin Klaassen44595162015-05-28 17:55:20 -07001034 mResultText.clear();
Justin Klaassen2be4fdb2014-08-06 19:54:09 -07001035 }
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001036 }
1037
Hans Boehm84614952014-11-25 18:46:17 -08001038 // Animate movement of result into the top formula slot.
1039 // Result window now remains translated in the top slot while the result is displayed.
1040 // (We convert it back to formula use only when the user provides new input.)
Justin Klaassen44595162015-05-28 17:55:20 -07001041 // Historical note: In the Lollipop version, this invisibly and instantaneously moved
Hans Boehm84614952014-11-25 18:46:17 -08001042 // formula and result displays back at the end of the animation. We no longer do that,
1043 // so that we can continue to properly support scrolling of the result.
1044 // We assume the result already contains the text to be expanded.
1045 private void onResult(boolean animate) {
Justin Klaassen44595162015-05-28 17:55:20 -07001046 // Calculate the textSize that would be used to display the result in the formula.
1047 // For scrollable results just use the minimum textSize to maximize the number of digits
1048 // that are visible on screen.
1049 float textSize = mFormulaText.getMinimumTextSize();
1050 if (!mResultText.isScrollable()) {
1051 textSize = mFormulaText.getVariableTextSize(mResultText.getText().toString());
1052 }
1053
1054 // Scale the result to match the calculated textSize, minimizing the jump-cut transition
1055 // when a result is reused in a subsequent expression.
1056 final float resultScale = textSize / mResultText.getTextSize();
1057
1058 // Set the result's pivot to match its gravity.
1059 mResultText.setPivotX(mResultText.getWidth() - mResultText.getPaddingRight());
1060 mResultText.setPivotY(mResultText.getHeight() - mResultText.getPaddingBottom());
1061
1062 // Calculate the necessary translations so the result takes the place of the formula and
1063 // the formula moves off the top of the screen.
Annie Chin28589dc2016-06-09 17:50:51 -07001064 final float resultTranslationY = (mFormulaContainer.getBottom() - mResultText.getBottom())
1065 - (mFormulaText.getPaddingBottom() - mResultText.getPaddingBottom());
1066 float formulaTranslationY = -mFormulaContainer.getBottom();
Annie Chin26e159e2016-05-18 15:17:14 -07001067 if (mOneLine) {
1068 // Position the result text.
1069 mResultText.setY(mResultText.getBottom());
Annie Chin28589dc2016-06-09 17:50:51 -07001070 formulaTranslationY = -(findViewById(R.id.toolbar).getBottom()
1071 + mFormulaContainer.getBottom());
Annie Chin26e159e2016-05-18 15:17:14 -07001072 }
Justin Klaassen4b3af052014-05-27 17:53:10 -07001073
Justin Klaassen44595162015-05-28 17:55:20 -07001074 // Change the result's textColor to match the formula.
1075 final int formulaTextColor = mFormulaText.getCurrentTextColor();
1076
Hans Boehm84614952014-11-25 18:46:17 -08001077 if (animate) {
Hans Boehmccc55662015-07-07 14:16:59 -07001078 mResultText.announceForAccessibility(getResources().getString(R.string.desc_eq));
1079 mResultText.announceForAccessibility(mResultText.getText());
Hans Boehmc1ea0912015-06-19 15:05:07 -07001080 setState(CalculatorState.ANIMATE);
Hans Boehm84614952014-11-25 18:46:17 -08001081 final AnimatorSet animatorSet = new AnimatorSet();
1082 animatorSet.playTogether(
Justin Klaassen44595162015-05-28 17:55:20 -07001083 ObjectAnimator.ofPropertyValuesHolder(mResultText,
1084 PropertyValuesHolder.ofFloat(View.SCALE_X, resultScale),
1085 PropertyValuesHolder.ofFloat(View.SCALE_Y, resultScale),
1086 PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, resultTranslationY)),
1087 ObjectAnimator.ofArgb(mResultText, TEXT_COLOR, formulaTextColor),
Annie Chine918fd22016-03-09 11:07:54 -08001088 ObjectAnimator.ofFloat(mFormulaContainer, View.TRANSLATION_Y,
1089 formulaTranslationY));
Justin Klaassen44595162015-05-28 17:55:20 -07001090 animatorSet.setDuration(getResources().getInteger(
1091 android.R.integer.config_longAnimTime));
Hans Boehm84614952014-11-25 18:46:17 -08001092 animatorSet.addListener(new AnimatorListenerAdapter() {
1093 @Override
Hans Boehm84614952014-11-25 18:46:17 -08001094 public void onAnimationEnd(Animator animation) {
Hans Boehm8f051c32016-10-03 16:53:58 -07001095 // Add current result to history.
1096 mEvaluator.preserve(true);
Hans Boehm84614952014-11-25 18:46:17 -08001097 setState(CalculatorState.RESULT);
1098 mCurrentAnimator = null;
1099 }
1100 });
Justin Klaassen4b3af052014-05-27 17:53:10 -07001101
Hans Boehm84614952014-11-25 18:46:17 -08001102 mCurrentAnimator = animatorSet;
1103 animatorSet.start();
Hans Boehm8f051c32016-10-03 16:53:58 -07001104 } else /* No animation desired; get there fast when restarting */ {
Justin Klaassen44595162015-05-28 17:55:20 -07001105 mResultText.setScaleX(resultScale);
1106 mResultText.setScaleY(resultScale);
1107 mResultText.setTranslationY(resultTranslationY);
1108 mResultText.setTextColor(formulaTextColor);
Annie Chine918fd22016-03-09 11:07:54 -08001109 mFormulaContainer.setTranslationY(formulaTranslationY);
Hans Boehm8f051c32016-10-03 16:53:58 -07001110 mEvaluator.represerve();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001111 setState(CalculatorState.RESULT);
Hans Boehm84614952014-11-25 18:46:17 -08001112 }
Justin Klaassen4b3af052014-05-27 17:53:10 -07001113 }
Hans Boehm84614952014-11-25 18:46:17 -08001114
1115 // Restore positions of the formula and result displays back to their original,
1116 // pre-animation state.
1117 private void restoreDisplayPositions() {
1118 // Clear result.
Justin Klaassen44595162015-05-28 17:55:20 -07001119 mResultText.setText("");
Hans Boehm84614952014-11-25 18:46:17 -08001120 // Reset all of the values modified during the animation.
Justin Klaassen44595162015-05-28 17:55:20 -07001121 mResultText.setScaleX(1.0f);
1122 mResultText.setScaleY(1.0f);
1123 mResultText.setTranslationX(0.0f);
1124 mResultText.setTranslationY(0.0f);
Annie Chine918fd22016-03-09 11:07:54 -08001125 mFormulaContainer.setTranslationY(0.0f);
Hans Boehm84614952014-11-25 18:46:17 -08001126
Hans Boehm08e8f322015-04-21 13:18:38 -07001127 mFormulaText.requestFocus();
Hans Boehm5e6a0ca2015-09-22 17:09:01 -07001128 }
1129
1130 @Override
1131 public void onClick(AlertDialogFragment fragment, int which) {
1132 if (which == DialogInterface.BUTTON_POSITIVE) {
1133 // Timeout extension request.
Hans Boehm8f051c32016-10-03 16:53:58 -07001134 mEvaluator.setLongTimeout();
Hans Boehm5e6a0ca2015-09-22 17:09:01 -07001135 }
1136 }
Hans Boehm84614952014-11-25 18:46:17 -08001137
Justin Klaassend48b7562015-04-16 16:51:38 -07001138 @Override
1139 public boolean onCreateOptionsMenu(Menu menu) {
Justin Klaassend36d63e2015-05-05 12:59:36 -07001140 super.onCreateOptionsMenu(menu);
1141
1142 getMenuInflater().inflate(R.menu.activity_calculator, menu);
Justin Klaassend48b7562015-04-16 16:51:38 -07001143 return true;
1144 }
1145
1146 @Override
1147 public boolean onPrepareOptionsMenu(Menu menu) {
Justin Klaassend36d63e2015-05-05 12:59:36 -07001148 super.onPrepareOptionsMenu(menu);
1149
1150 // Show the leading option when displaying a result.
1151 menu.findItem(R.id.menu_leading).setVisible(mCurrentState == CalculatorState.RESULT);
1152
1153 // Show the fraction option when displaying a rational result.
1154 menu.findItem(R.id.menu_fraction).setVisible(mCurrentState == CalculatorState.RESULT
Hans Boehm8f051c32016-10-03 16:53:58 -07001155 && mEvaluator.getResult(Evaluator.MAIN_INDEX).exactlyDisplayable());
Justin Klaassend36d63e2015-05-05 12:59:36 -07001156
Justin Klaassend48b7562015-04-16 16:51:38 -07001157 return true;
Hans Boehm84614952014-11-25 18:46:17 -08001158 }
1159
1160 @Override
Justin Klaassend48b7562015-04-16 16:51:38 -07001161 public boolean onOptionsItemSelected(MenuItem item) {
Hans Boehm84614952014-11-25 18:46:17 -08001162 switch (item.getItemId()) {
Annie Chinabd202f2016-10-14 14:23:45 -07001163 case R.id.menu_history:
Annie Chin09547532016-10-14 10:59:07 -07001164 showHistoryFragment(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
1165 mDragLayout.setOpen();
Annie Chinabd202f2016-10-14 14:23:45 -07001166 return true;
Justin Klaassend36d63e2015-05-05 12:59:36 -07001167 case R.id.menu_leading:
1168 displayFull();
Hans Boehm84614952014-11-25 18:46:17 -08001169 return true;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001170 case R.id.menu_fraction:
1171 displayFraction();
1172 return true;
Justin Klaassend36d63e2015-05-05 12:59:36 -07001173 case R.id.menu_licenses:
1174 startActivity(new Intent(this, Licenses.class));
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001175 return true;
Hans Boehm84614952014-11-25 18:46:17 -08001176 default:
1177 return super.onOptionsItemSelected(item);
1178 }
1179 }
1180
Annie Chin09547532016-10-14 10:59:07 -07001181 private void showHistoryFragment(int transit) {
Annie Chin06fd3cf2016-11-07 16:04:33 -08001182 final FragmentManager manager = getFragmentManager();
1183 if (manager == null || manager.isDestroyed()) {
1184 return;
1185 }
Annie Chind0f87d22016-10-24 09:04:12 -07001186 if (!mDragLayout.isOpen()) {
1187 getFragmentManager().beginTransaction()
1188 .replace(R.id.history_frame, mHistoryFragment, HistoryFragment.TAG)
1189 .setTransition(transit)
1190 .addToBackStack(HistoryFragment.TAG)
1191 .commit();
1192 }
Annie Chin06fd3cf2016-11-07 16:04:33 -08001193 // TODO: pass current scroll position of result
Annie Chin09547532016-10-14 10:59:07 -07001194 }
1195
Christine Franks7452d3a2016-10-27 13:41:18 -07001196 private void displayMessage(String title, String message) {
1197 AlertDialogFragment.showMessageDialog(this, title, message, null);
Hans Boehm84614952014-11-25 18:46:17 -08001198 }
1199
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001200 private void displayFraction() {
Hans Boehm8f051c32016-10-03 16:53:58 -07001201 UnifiedReal result = mEvaluator.getResult(Evaluator.MAIN_INDEX);
Christine Franks7452d3a2016-10-27 13:41:18 -07001202 displayMessage(getString(R.string.menu_fraction),
1203 KeyMaps.translateResult(result.toNiceString()));
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001204 }
1205
1206 // Display full result to currently evaluated precision
1207 private void displayFull() {
1208 Resources res = getResources();
Hans Boehm24c91ed2016-06-30 18:53:44 -07001209 String msg = mResultText.getFullText(true /* withSeparators */) + " ";
Justin Klaassen44595162015-05-28 17:55:20 -07001210 if (mResultText.fullTextIsExact()) {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001211 msg += res.getString(R.string.exact);
1212 } else {
1213 msg += res.getString(R.string.approximate);
1214 }
Christine Franks7452d3a2016-10-27 13:41:18 -07001215 displayMessage(getString(R.string.menu_leading), msg);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001216 }
1217
Hans Boehm017de982015-06-10 17:46:03 -07001218 /**
1219 * Add input characters to the end of the expression.
1220 * Map them to the appropriate button pushes when possible. Leftover characters
1221 * are added to mUnprocessedChars, which is presumed to immediately precede the newly
1222 * added characters.
Hans Boehm65a99a42016-02-03 18:16:07 -08001223 * @param moreChars characters to be added
1224 * @param explicit these characters were explicitly typed by the user, not pasted
Hans Boehm017de982015-06-10 17:46:03 -07001225 */
1226 private void addChars(String moreChars, boolean explicit) {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001227 if (mUnprocessedChars != null) {
1228 moreChars = mUnprocessedChars + moreChars;
1229 }
1230 int current = 0;
1231 int len = moreChars.length();
Hans Boehm0b9806f2015-06-29 16:07:15 -07001232 boolean lastWasDigit = false;
Hans Boehm5d79d102015-09-16 16:33:47 -07001233 if (mCurrentState == CalculatorState.RESULT && len != 0) {
1234 // Clear display immediately for incomplete function name.
1235 switchToInput(KeyMaps.keyForChar(moreChars.charAt(current)));
1236 }
Hans Boehm24c91ed2016-06-30 18:53:44 -07001237 char groupingSeparator = KeyMaps.translateResult(",").charAt(0);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001238 while (current < len) {
1239 char c = moreChars.charAt(current);
Hans Boehm24c91ed2016-06-30 18:53:44 -07001240 if (Character.isSpaceChar(c) || c == groupingSeparator) {
1241 ++current;
1242 continue;
1243 }
Hans Boehm013969e2015-04-13 20:29:47 -07001244 int k = KeyMaps.keyForChar(c);
Hans Boehm0b9806f2015-06-29 16:07:15 -07001245 if (!explicit) {
1246 int expEnd;
1247 if (lastWasDigit && current !=
1248 (expEnd = Evaluator.exponentEnd(moreChars, current))) {
1249 // Process scientific notation with 'E' when pasting, in spite of ambiguity
1250 // with base of natural log.
1251 // Otherwise the 10^x key is the user's friend.
1252 mEvaluator.addExponent(moreChars, current, expEnd);
1253 current = expEnd;
1254 lastWasDigit = false;
1255 continue;
1256 } else {
1257 boolean isDigit = KeyMaps.digVal(k) != KeyMaps.NOT_DIGIT;
1258 if (current == 0 && (isDigit || k == R.id.dec_point)
Hans Boehm8f051c32016-10-03 16:53:58 -07001259 && mEvaluator.getExpr(Evaluator.MAIN_INDEX).hasTrailingConstant()) {
Hans Boehm0b9806f2015-06-29 16:07:15 -07001260 // Refuse to concatenate pasted content to trailing constant.
1261 // This makes pasting of calculator results more consistent, whether or
1262 // not the old calculator instance is still around.
1263 addKeyToExpr(R.id.op_mul);
1264 }
1265 lastWasDigit = (isDigit || lastWasDigit && k == R.id.dec_point);
1266 }
1267 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001268 if (k != View.NO_ID) {
1269 mCurrentButton = findViewById(k);
Hans Boehm017de982015-06-10 17:46:03 -07001270 if (explicit) {
1271 addExplicitKeyToExpr(k);
1272 } else {
1273 addKeyToExpr(k);
1274 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001275 if (Character.isSurrogate(c)) {
1276 current += 2;
1277 } else {
1278 ++current;
1279 }
1280 continue;
1281 }
Hans Boehm013969e2015-04-13 20:29:47 -07001282 int f = KeyMaps.funForString(moreChars, current);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001283 if (f != View.NO_ID) {
1284 mCurrentButton = findViewById(f);
Hans Boehm017de982015-06-10 17:46:03 -07001285 if (explicit) {
1286 addExplicitKeyToExpr(f);
1287 } else {
1288 addKeyToExpr(f);
1289 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001290 if (f == R.id.op_sqrt) {
1291 // Square root entered as function; don't lose the parenthesis.
1292 addKeyToExpr(R.id.lparen);
1293 }
1294 current = moreChars.indexOf('(', current) + 1;
1295 continue;
1296 }
1297 // There are characters left, but we can't convert them to button presses.
1298 mUnprocessedChars = moreChars.substring(current);
1299 redisplayAfterFormulaChange();
Hans Boehm52d477a2016-04-01 17:42:50 -07001300 showOrHideToolbar();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001301 return;
1302 }
1303 mUnprocessedChars = null;
1304 redisplayAfterFormulaChange();
Hans Boehm52d477a2016-04-01 17:42:50 -07001305 showOrHideToolbar();
Hans Boehm84614952014-11-25 18:46:17 -08001306 }
1307
Hans Boehm8f051c32016-10-03 16:53:58 -07001308 private void clearIfNotInputState() {
1309 if (mCurrentState == CalculatorState.ERROR
1310 || mCurrentState == CalculatorState.RESULT) {
1311 setState(CalculatorState.INPUT);
1312 mEvaluator.clearMain();
1313 }
1314 }
1315
Annie Chind0f87d22016-10-24 09:04:12 -07001316 private boolean isViewTarget(View view, MotionEvent event) {
1317 mHitRect.set(0, 0, view.getWidth(), view.getHeight());
1318 mDragLayout.offsetDescendantRectToMyCoords(view, mHitRect);
1319 return mHitRect.contains((int) event.getX(), (int) event.getY());
1320 }
1321
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001322 @Override
Justin Klaassenfc5ac822015-06-18 13:15:17 -07001323 public boolean onPaste(ClipData clip) {
1324 final ClipData.Item item = clip.getItemCount() == 0 ? null : clip.getItemAt(0);
1325 if (item == null) {
1326 // nothing to paste, bail early...
1327 return false;
1328 }
1329
1330 // Check if the item is a previously copied result, otherwise paste as raw text.
1331 final Uri uri = item.getUri();
1332 if (uri != null && mEvaluator.isLastSaved(uri)) {
Hans Boehm8f051c32016-10-03 16:53:58 -07001333 clearIfNotInputState();
1334 mEvaluator.appendExpr(mEvaluator.getSavedIndex());
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001335 redisplayAfterFormulaChange();
Justin Klaassenfc5ac822015-06-18 13:15:17 -07001336 } else {
Hans Boehm017de982015-06-10 17:46:03 -07001337 addChars(item.coerceToText(this).toString(), false);
Hans Boehm84614952014-11-25 18:46:17 -08001338 }
Justin Klaassenfc5ac822015-06-18 13:15:17 -07001339 return true;
Hans Boehm84614952014-11-25 18:46:17 -08001340 }
Chenjie Yu3937b652016-06-01 23:14:26 -07001341
1342 /**
1343 * Clean up animation for context menu.
1344 */
1345 @Override
1346 public void onContextMenuClosed(Menu menu) {
1347 stopActionModeOrContextMenu();
1348 }
Justin Klaassen4b3af052014-05-27 17:53:10 -07001349}