blob: e465dcf26308cc510790f6c09c1473f42f77ec35 [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 Chin09547532016-10-14 10:59:07 -070036import android.app.FragmentTransaction;
Justin Klaassenfc5ac822015-06-18 13:15:17 -070037import android.content.ClipData;
Hans Boehm5e6a0ca2015-09-22 17:09:01 -070038import android.content.DialogInterface;
Justin Klaassend36d63e2015-05-05 12:59:36 -070039import android.content.Intent;
Hans Boehmbfe8c222015-04-02 16:26:07 -070040import android.content.res.Resources;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070041import android.graphics.Color;
Justin Klaassen8fff1442014-06-19 10:43:29 -070042import android.graphics.Rect;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070043import android.net.Uri;
Justin Klaassen4b3af052014-05-27 17:53:10 -070044import android.os.Bundle;
Justin Klaassenf79d6f62014-08-26 12:27:08 -070045import android.support.annotation.NonNull;
Chenjie Yu3937b652016-06-01 23:14:26 -070046import android.support.v4.content.ContextCompat;
Justin Klaassen3b4d13d2014-06-06 18:18:37 +010047import android.support.v4.view.ViewPager;
Annie Chine918fd22016-03-09 11:07:54 -080048import android.text.Editable;
Hans Boehm8a4f81c2015-07-09 10:41:25 -070049import android.text.SpannableStringBuilder;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070050import android.text.Spanned;
Annie Chinf360ef02016-03-10 13:45:39 -080051import android.text.TextUtils;
Annie Chine918fd22016-03-09 11:07:54 -080052import android.text.TextWatcher;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070053import android.text.style.ForegroundColorSpan;
Justin Klaassen44595162015-05-28 17:55:20 -070054import android.util.Property;
Annie Chine918fd22016-03-09 11:07:54 -080055import android.view.ActionMode;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070056import android.view.KeyCharacterMap;
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -070057import android.view.KeyEvent;
Hans Boehm84614952014-11-25 18:46:17 -080058import android.view.Menu;
59import android.view.MenuItem;
Annie Chind0f87d22016-10-24 09:04:12 -070060import android.view.MotionEvent;
Justin Klaassen4b3af052014-05-27 17:53:10 -070061import android.view.View;
62import android.view.View.OnLongClickListener;
Justin Klaassen5f2a3342014-06-11 17:40:22 -070063import android.view.ViewAnimationUtils;
Justin Klaassen8fff1442014-06-19 10:43:29 -070064import android.view.ViewGroupOverlay;
Annie Chine918fd22016-03-09 11:07:54 -080065import android.view.ViewTreeObserver;
Justin Klaassen4b3af052014-05-27 17:53:10 -070066import android.view.animation.AccelerateDecelerateInterpolator;
Annie Chind0f87d22016-10-24 09:04:12 -070067import android.widget.FrameLayout;
Annie Chine918fd22016-03-09 11:07:54 -080068import android.widget.HorizontalScrollView;
Justin Klaassenfed941a2014-06-09 18:42:40 +010069import android.widget.TextView;
Justin Klaassend48b7562015-04-16 16:51:38 -070070import android.widget.Toolbar;
Justin Klaassenfed941a2014-06-09 18:42:40 +010071
Christine Franks7452d3a2016-10-27 13:41:18 -070072import com.android.calculator2.CalculatorFormula.OnTextSizeChangeListener;
Hans Boehm84614952014-11-25 18:46:17 -080073
74import java.io.ByteArrayInputStream;
Hans Boehm84614952014-11-25 18:46:17 -080075import java.io.ByteArrayOutputStream;
Hans Boehm84614952014-11-25 18:46:17 -080076import java.io.IOException;
Justin Klaassen721ec842015-05-28 14:30:08 -070077import java.io.ObjectInput;
78import java.io.ObjectInputStream;
79import java.io.ObjectOutput;
80import java.io.ObjectOutputStream;
Justin Klaassen4b3af052014-05-27 17:53:10 -070081
Hans Boehm8f051c32016-10-03 16:53:58 -070082public class Calculator extends Activity
83 implements OnTextSizeChangeListener, OnLongClickListener, CalculatorFormula.OnPasteListener,
84 AlertDialogFragment.OnClickListener, Evaluator.EvaluationListener /* for main result */ {
Justin Klaassen2be4fdb2014-08-06 19:54:09 -070085
86 /**
87 * Constant for an invalid resource id.
88 */
89 public static final int INVALID_RES_ID = -1;
Justin Klaassen4b3af052014-05-27 17:53:10 -070090
91 private enum CalculatorState {
Hans Boehm84614952014-11-25 18:46:17 -080092 INPUT, // Result and formula both visible, no evaluation requested,
93 // Though result may be visible on bottom line.
94 EVALUATE, // Both visible, evaluation requested, evaluation/animation incomplete.
Hans Boehmc1ea0912015-06-19 15:05:07 -070095 // Not used for instant result evaluation.
Hans Boehm84614952014-11-25 18:46:17 -080096 INIT, // Very temporary state used as alternative to EVALUATE
97 // during reinitialization. Do not animate on completion.
98 ANIMATE, // Result computed, animation to enlarge result window in progress.
99 RESULT, // Result displayed, formula invisible.
100 // If we are in RESULT state, the formula was evaluated without
101 // error to initial precision.
Hans Boehm8f051c32016-10-03 16:53:58 -0700102 // The current formula is now also the last history entry.
Hans Boehm84614952014-11-25 18:46:17 -0800103 ERROR // Error displayed: Formula visible, result shows error message.
104 // Display similar to INPUT state.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700105 }
Hans Boehm84614952014-11-25 18:46:17 -0800106 // Normal transition sequence is
107 // INPUT -> EVALUATE -> ANIMATE -> RESULT (or ERROR) -> INPUT
108 // A RESULT -> ERROR transition is possible in rare corner cases, in which
109 // a higher precision evaluation exposes an error. This is possible, since we
110 // initially evaluate assuming we were given a well-defined problem. If we
111 // were actually asked to compute sqrt(<extremely tiny negative number>) we produce 0
112 // unless we are asked for enough precision that we can distinguish the argument from zero.
113 // TODO: Consider further heuristics to reduce the chance of observing this?
114 // It already seems to be observable only in contrived cases.
115 // ANIMATE, ERROR, and RESULT are translated to an INIT state if the application
116 // is restarted in that state. This leads us to recompute and redisplay the result
117 // ASAP.
118 // TODO: Possibly save a bit more information, e.g. its initial display string
119 // or most significant digit position, to speed up restart.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700120
Justin Klaassen44595162015-05-28 17:55:20 -0700121 private final Property<TextView, Integer> TEXT_COLOR =
122 new Property<TextView, Integer>(Integer.class, "textColor") {
123 @Override
124 public Integer get(TextView textView) {
125 return textView.getCurrentTextColor();
126 }
127
128 @Override
129 public void set(TextView textView, Integer textColor) {
130 textView.setTextColor(textColor);
131 }
132 };
133
Justin Klaassen3e223ea2016-02-05 14:18:06 -0800134 private static final String NAME = "Calculator";
Hans Boehm84614952014-11-25 18:46:17 -0800135 private static final String KEY_DISPLAY_STATE = NAME + "_display_state";
Hans Boehm760a9dc2015-04-20 10:27:12 -0700136 private static final String KEY_UNPROCESSED_CHARS = NAME + "_unprocessed_chars";
Justin Klaassen3e223ea2016-02-05 14:18:06 -0800137 /**
138 * Associated value is a byte array holding the evaluator state.
139 */
Hans Boehm84614952014-11-25 18:46:17 -0800140 private static final String KEY_EVAL_STATE = NAME + "_eval_state";
Justin Klaassen3e223ea2016-02-05 14:18:06 -0800141 private static final String KEY_INVERSE_MODE = NAME + "_inverse_mode";
Christine Frankseeff27f2016-07-29 12:05:29 -0700142 /**
143 * Associated value is an boolean holding the visibility state of the toolbar.
144 */
145 private static final String KEY_SHOW_TOOLBAR = NAME + "_show_toolbar";
Justin Klaassen741471e2014-06-11 09:43:44 -0700146
Annie Chine918fd22016-03-09 11:07:54 -0800147 private final ViewTreeObserver.OnPreDrawListener mPreDrawListener =
148 new ViewTreeObserver.OnPreDrawListener() {
149 @Override
150 public boolean onPreDraw() {
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700151 mFormulaContainer.scrollTo(mFormulaText.getRight(), 0);
Annie Chine918fd22016-03-09 11:07:54 -0800152 final ViewTreeObserver observer = mFormulaContainer.getViewTreeObserver();
153 if (observer.isAlive()) {
154 observer.removeOnPreDrawListener(this);
155 }
156 return false;
157 }
158 };
159
160 private final TextWatcher mFormulaTextWatcher = new TextWatcher() {
161 @Override
162 public void beforeTextChanged(CharSequence charSequence, int start, int count, int after) {
163 }
164
165 @Override
166 public void onTextChanged(CharSequence charSequence, int start, int count, int after) {
167 }
168
169 @Override
170 public void afterTextChanged(Editable editable) {
171 final ViewTreeObserver observer = mFormulaContainer.getViewTreeObserver();
172 if (observer.isAlive()) {
173 observer.removeOnPreDrawListener(mPreDrawListener);
174 observer.addOnPreDrawListener(mPreDrawListener);
175 }
176 }
177 };
178
Annie Chind0f87d22016-10-24 09:04:12 -0700179 private final DragLayout.DragCallback mDragCallback = new DragLayout.DragCallback() {
180 @Override
181 public void onStartDragging() {
182 showHistoryFragment(FragmentTransaction.TRANSIT_NONE);
183 }
184
185 @Override
186 public void whileDragging(float yFraction) {
187 // no-op
188 }
189
190 @Override
191 public void onClosed() {
192 getFragmentManager().popBackStack();
193 }
194
195 @Override
196 public boolean allowDrag(MotionEvent event) {
197 return isViewTarget(mHistoryFrame, event) || isViewTarget(mDisplayView, event);
198 }
199
200 @Override
201 public boolean shouldInterceptTouchEvent(MotionEvent event) {
202 return isViewTarget(mHistoryFrame, event) || isViewTarget(mDisplayView, event);
203 }
204
205 @Override
206 public int getDisplayHeight() {
207 return mDisplayView.getMeasuredHeight();
208 }
209
210 public void onLayout(int translation) {
211 mHistoryFrame.setTranslationY(translation + mDisplayView.getBottom());
212 }
213 };
214
215 private final Rect mHitRect = new Rect();
216
Justin Klaassen4b3af052014-05-27 17:53:10 -0700217 private CalculatorState mCurrentState;
Hans Boehm84614952014-11-25 18:46:17 -0800218 private Evaluator mEvaluator;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700219
Justin Klaassen9d33cdc2016-02-21 14:16:14 -0800220 private CalculatorDisplay mDisplayView;
Justin Klaassend48b7562015-04-16 16:51:38 -0700221 private TextView mModeView;
Christine Franks7452d3a2016-10-27 13:41:18 -0700222 private CalculatorFormula mFormulaText;
Justin Klaassen44595162015-05-28 17:55:20 -0700223 private CalculatorResult mResultText;
Annie Chine918fd22016-03-09 11:07:54 -0800224 private HorizontalScrollView mFormulaContainer;
Annie Chin09547532016-10-14 10:59:07 -0700225 private DragLayout mDragLayout;
Annie Chind0f87d22016-10-24 09:04:12 -0700226 private FrameLayout mHistoryFrame;
Justin Klaassend48b7562015-04-16 16:51:38 -0700227
Justin Klaassen3b4d13d2014-06-06 18:18:37 +0100228 private ViewPager mPadViewPager;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700229 private View mDeleteButton;
230 private View mClearButton;
Justin Klaassend48b7562015-04-16 16:51:38 -0700231 private View mEqualButton;
Justin Klaassene2711cb2015-05-28 11:13:17 -0700232
233 private TextView mInverseToggle;
234 private TextView mModeToggle;
235
Justin Klaassen721ec842015-05-28 14:30:08 -0700236 private View[] mInvertibleButtons;
Justin Klaassene2711cb2015-05-28 11:13:17 -0700237 private View[] mInverseButtons;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700238
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700239 private View mCurrentButton;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700240 private Animator mCurrentAnimator;
241
Hans Boehm8a4f81c2015-07-09 10:41:25 -0700242 // Characters that were recently entered at the end of the display that have not yet
243 // been added to the underlying expression.
244 private String mUnprocessedChars = null;
245
246 // Color to highlight unprocessed characters from physical keyboard.
247 // TODO: should probably match this to the error color?
248 private ForegroundColorSpan mUnprocessedColorSpan = new ForegroundColorSpan(Color.RED);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700249
Annie Chin26e159e2016-05-18 15:17:14 -0700250 // Whether the display is one line.
251 private boolean mOneLine;
252
Annie Chin09547532016-10-14 10:59:07 -0700253 private HistoryFragment mHistoryFragment = new HistoryFragment();
254
Hans Boehm8f051c32016-10-03 16:53:58 -0700255 // The user requested that the result currently being evaluated should be stored to "memory".
256 private boolean mStoreToMemoryRequested = false;
257
Justin Klaassen4b3af052014-05-27 17:53:10 -0700258 @Override
259 protected void onCreate(Bundle savedInstanceState) {
260 super.onCreate(savedInstanceState);
Annie Chin09547532016-10-14 10:59:07 -0700261 setContentView(R.layout.activity_calculator_main);
Justin Klaassend48b7562015-04-16 16:51:38 -0700262 setActionBar((Toolbar) findViewById(R.id.toolbar));
263
264 // Hide all default options in the ActionBar.
265 getActionBar().setDisplayOptions(0);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700266
Justin Klaassen9d33cdc2016-02-21 14:16:14 -0800267 // Ensure the toolbar stays visible while the options menu is displayed.
268 getActionBar().addOnMenuVisibilityListener(new ActionBar.OnMenuVisibilityListener() {
269 @Override
270 public void onMenuVisibilityChanged(boolean isVisible) {
271 mDisplayView.setForceToolbarVisible(isVisible);
272 }
273 });
274
275 mDisplayView = (CalculatorDisplay) findViewById(R.id.display);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700276 mModeView = (TextView) findViewById(R.id.mode);
Christine Franks7452d3a2016-10-27 13:41:18 -0700277 mFormulaText = (CalculatorFormula) findViewById(R.id.formula);
Justin Klaassen44595162015-05-28 17:55:20 -0700278 mResultText = (CalculatorResult) findViewById(R.id.result);
Annie Chine918fd22016-03-09 11:07:54 -0800279 mFormulaContainer = (HorizontalScrollView) findViewById(R.id.formula_container);
Justin Klaassend48b7562015-04-16 16:51:38 -0700280
Justin Klaassen3b4d13d2014-06-06 18:18:37 +0100281 mPadViewPager = (ViewPager) findViewById(R.id.pad_pager);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700282 mDeleteButton = findViewById(R.id.del);
283 mClearButton = findViewById(R.id.clr);
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700284 mEqualButton = findViewById(R.id.pad_numeric).findViewById(R.id.eq);
285 if (mEqualButton == null || mEqualButton.getVisibility() != View.VISIBLE) {
286 mEqualButton = findViewById(R.id.pad_operator).findViewById(R.id.eq);
287 }
Justin Klaassene2711cb2015-05-28 11:13:17 -0700288
289 mInverseToggle = (TextView) findViewById(R.id.toggle_inv);
290 mModeToggle = (TextView) findViewById(R.id.toggle_mode);
291
Annie Chin26e159e2016-05-18 15:17:14 -0700292 mOneLine = mResultText.getVisibility() == View.INVISIBLE;
293
Justin Klaassen721ec842015-05-28 14:30:08 -0700294 mInvertibleButtons = new View[] {
295 findViewById(R.id.fun_sin),
296 findViewById(R.id.fun_cos),
Hans Boehm4db31b42015-05-31 12:19:05 -0700297 findViewById(R.id.fun_tan),
298 findViewById(R.id.fun_ln),
299 findViewById(R.id.fun_log),
300 findViewById(R.id.op_sqrt)
Justin Klaassene2711cb2015-05-28 11:13:17 -0700301 };
302 mInverseButtons = new View[] {
303 findViewById(R.id.fun_arcsin),
304 findViewById(R.id.fun_arccos),
Hans Boehm4db31b42015-05-31 12:19:05 -0700305 findViewById(R.id.fun_arctan),
306 findViewById(R.id.fun_exp),
307 findViewById(R.id.fun_10pow),
308 findViewById(R.id.op_sqr)
Justin Klaassene2711cb2015-05-28 11:13:17 -0700309 };
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700310
Hans Boehm8f051c32016-10-03 16:53:58 -0700311 mEvaluator = new Evaluator(this);
312 mResultText.setEvaluator(mEvaluator, Evaluator.MAIN_INDEX);
Hans Boehm013969e2015-04-13 20:29:47 -0700313 KeyMaps.setActivity(this);
Justin Klaassen2be4fdb2014-08-06 19:54:09 -0700314
Annie Chin09547532016-10-14 10:59:07 -0700315 mDragLayout = (DragLayout) findViewById(R.id.drag_layout);
Annie Chind0f87d22016-10-24 09:04:12 -0700316 mDragLayout.removeDragCallback(mDragCallback);
317 mDragLayout.addDragCallback(mDragCallback);
Annie Chin09547532016-10-14 10:59:07 -0700318
Annie Chind0f87d22016-10-24 09:04:12 -0700319 mHistoryFrame = (FrameLayout) findViewById(R.id.history_frame);
Annie Chin09547532016-10-14 10:59:07 -0700320
Hans Boehm84614952014-11-25 18:46:17 -0800321 if (savedInstanceState != null) {
322 setState(CalculatorState.values()[
323 savedInstanceState.getInt(KEY_DISPLAY_STATE,
324 CalculatorState.INPUT.ordinal())]);
Hans Boehm760a9dc2015-04-20 10:27:12 -0700325 CharSequence unprocessed = savedInstanceState.getCharSequence(KEY_UNPROCESSED_CHARS);
326 if (unprocessed != null) {
327 mUnprocessedChars = unprocessed.toString();
328 }
329 byte[] state = savedInstanceState.getByteArray(KEY_EVAL_STATE);
Hans Boehm84614952014-11-25 18:46:17 -0800330 if (state != null) {
331 try (ObjectInput in = new ObjectInputStream(new ByteArrayInputStream(state))) {
332 mEvaluator.restoreInstanceState(in);
333 } catch (Throwable ignored) {
334 // When in doubt, revert to clean state
335 mCurrentState = CalculatorState.INPUT;
Hans Boehm8f051c32016-10-03 16:53:58 -0700336 mEvaluator.clearMain();
Hans Boehm84614952014-11-25 18:46:17 -0800337 }
338 }
Hans Boehmfbcef702015-04-27 18:07:47 -0700339 } else {
340 mCurrentState = CalculatorState.INPUT;
Hans Boehm8f051c32016-10-03 16:53:58 -0700341 mEvaluator.clearMain();
Hans Boehm84614952014-11-25 18:46:17 -0800342 }
Justin Klaassene2711cb2015-05-28 11:13:17 -0700343
Hans Boehm08e8f322015-04-21 13:18:38 -0700344 mFormulaText.setOnTextSizeChangeListener(this);
Justin Klaassenfc5ac822015-06-18 13:15:17 -0700345 mFormulaText.setOnPasteListener(this);
Annie Chine918fd22016-03-09 11:07:54 -0800346 mFormulaText.addTextChangedListener(mFormulaTextWatcher);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700347 mDeleteButton.setOnLongClickListener(this);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700348
Justin Klaassen3e223ea2016-02-05 14:18:06 -0800349 onInverseToggled(savedInstanceState != null
350 && savedInstanceState.getBoolean(KEY_INVERSE_MODE));
Christine Frankseeff27f2016-07-29 12:05:29 -0700351
Hans Boehm8f051c32016-10-03 16:53:58 -0700352 onModeChanged(mEvaluator.getDegreeMode(Evaluator.MAIN_INDEX));
Christine Frankseeff27f2016-07-29 12:05:29 -0700353 if (savedInstanceState != null &&
354 savedInstanceState.getBoolean(KEY_SHOW_TOOLBAR, true) == false) {
355 mDisplayView.hideToolbar();
356 } else {
357 showAndMaybeHideToolbar();
358 }
Justin Klaassene2711cb2015-05-28 11:13:17 -0700359
Hans Boehm84614952014-11-25 18:46:17 -0800360 if (mCurrentState != CalculatorState.INPUT) {
Hans Boehmfbcef702015-04-27 18:07:47 -0700361 // Just reevaluate.
362 redisplayFormula();
Hans Boehm84614952014-11-25 18:46:17 -0800363 setState(CalculatorState.INIT);
Hans Boehm8f051c32016-10-03 16:53:58 -0700364 mEvaluator.requireResult(Evaluator.MAIN_INDEX, this, mResultText);
Hans Boehm84614952014-11-25 18:46:17 -0800365 } else {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700366 redisplayAfterFormulaChange();
Hans Boehm84614952014-11-25 18:46:17 -0800367 }
368 // TODO: We're currently not saving and restoring scroll position.
369 // We probably should. Details may require care to deal with:
370 // - new display size
371 // - slow recomputation if we've scrolled far.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700372 }
373
374 @Override
Justin Klaassen9d33cdc2016-02-21 14:16:14 -0800375 protected void onResume() {
376 super.onResume();
Christine Frankseeff27f2016-07-29 12:05:29 -0700377 if (mDisplayView.isToolbarVisible()) {
378 showAndMaybeHideToolbar();
379 }
Justin Klaassen9d33cdc2016-02-21 14:16:14 -0800380 }
381
382 @Override
Justin Klaassenf79d6f62014-08-26 12:27:08 -0700383 protected void onSaveInstanceState(@NonNull Bundle outState) {
Hans Boehm40125442016-01-22 10:35:35 -0800384 mEvaluator.cancelAll(true);
Justin Klaassenf79d6f62014-08-26 12:27:08 -0700385 // If there's an animation in progress, cancel it first to ensure our state is up-to-date.
386 if (mCurrentAnimator != null) {
387 mCurrentAnimator.cancel();
388 }
389
Justin Klaassen4b3af052014-05-27 17:53:10 -0700390 super.onSaveInstanceState(outState);
Hans Boehm84614952014-11-25 18:46:17 -0800391 outState.putInt(KEY_DISPLAY_STATE, mCurrentState.ordinal());
Hans Boehm760a9dc2015-04-20 10:27:12 -0700392 outState.putCharSequence(KEY_UNPROCESSED_CHARS, mUnprocessedChars);
Hans Boehm84614952014-11-25 18:46:17 -0800393 ByteArrayOutputStream byteArrayStream = new ByteArrayOutputStream();
394 try (ObjectOutput out = new ObjectOutputStream(byteArrayStream)) {
395 mEvaluator.saveInstanceState(out);
396 } catch (IOException e) {
397 // Impossible; No IO involved.
398 throw new AssertionError("Impossible IO exception", e);
399 }
400 outState.putByteArray(KEY_EVAL_STATE, byteArrayStream.toByteArray());
Justin Klaassen3e223ea2016-02-05 14:18:06 -0800401 outState.putBoolean(KEY_INVERSE_MODE, mInverseToggle.isSelected());
Christine Frankseeff27f2016-07-29 12:05:29 -0700402 outState.putBoolean(KEY_SHOW_TOOLBAR, mDisplayView.isToolbarVisible());
Justin Klaassen4b3af052014-05-27 17:53:10 -0700403 }
404
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700405 // Set the state, updating delete label and display colors.
406 // This restores display positions on moving to INPUT.
Justin Klaassend48b7562015-04-16 16:51:38 -0700407 // But movement/animation for moving to RESULT has already been done.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700408 private void setState(CalculatorState state) {
409 if (mCurrentState != state) {
Hans Boehm84614952014-11-25 18:46:17 -0800410 if (state == CalculatorState.INPUT) {
411 restoreDisplayPositions();
412 }
Justin Klaassen4b3af052014-05-27 17:53:10 -0700413 mCurrentState = state;
414
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700415 if (mCurrentState == CalculatorState.RESULT) {
416 // No longer do this for ERROR; allow mistakes to be corrected.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700417 mDeleteButton.setVisibility(View.GONE);
418 mClearButton.setVisibility(View.VISIBLE);
419 } else {
420 mDeleteButton.setVisibility(View.VISIBLE);
421 mClearButton.setVisibility(View.GONE);
422 }
423
Annie Chin26e159e2016-05-18 15:17:14 -0700424 if (mOneLine) {
425 if (mCurrentState == CalculatorState.RESULT
426 || mCurrentState == CalculatorState.EVALUATE
427 || mCurrentState == CalculatorState.ANIMATE) {
428 mFormulaText.setVisibility(View.VISIBLE);
429 mResultText.setVisibility(View.VISIBLE);
Annie Chin947d93b2016-06-14 10:18:54 -0700430 } else if (mCurrentState == CalculatorState.ERROR) {
431 mFormulaText.setVisibility(View.INVISIBLE);
432 mResultText.setVisibility(View.VISIBLE);
Annie Chin26e159e2016-05-18 15:17:14 -0700433 } else {
434 mFormulaText.setVisibility(View.VISIBLE);
435 mResultText.setVisibility(View.INVISIBLE);
436 }
437 }
438
Hans Boehm84614952014-11-25 18:46:17 -0800439 if (mCurrentState == CalculatorState.ERROR) {
Chenjie Yu3937b652016-06-01 23:14:26 -0700440 final int errorColor =
441 ContextCompat.getColor(this, R.color.calculator_error_color);
Hans Boehm08e8f322015-04-21 13:18:38 -0700442 mFormulaText.setTextColor(errorColor);
Justin Klaassen44595162015-05-28 17:55:20 -0700443 mResultText.setTextColor(errorColor);
Justin Klaassen8fff1442014-06-19 10:43:29 -0700444 getWindow().setStatusBarColor(errorColor);
Justin Klaassen44595162015-05-28 17:55:20 -0700445 } else if (mCurrentState != CalculatorState.RESULT) {
Chenjie Yu3937b652016-06-01 23:14:26 -0700446 mFormulaText.setTextColor(
447 ContextCompat.getColor(this, R.color.display_formula_text_color));
448 mResultText.setTextColor(
449 ContextCompat.getColor(this, R.color.display_result_text_color));
450 getWindow().setStatusBarColor(
451 ContextCompat.getColor(this, R.color.calculator_accent_color));
Justin Klaassen4b3af052014-05-27 17:53:10 -0700452 }
Justin Klaassend48b7562015-04-16 16:51:38 -0700453
454 invalidateOptionsMenu();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700455 }
456 }
457
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700458 @Override
Annie Chind0f87d22016-10-24 09:04:12 -0700459 protected void onDestroy() {
460 mDragLayout.removeDragCallback(mDragCallback);
461 super.onDestroy();
462 }
463
464 @Override
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700465 public void onActionModeStarted(ActionMode mode) {
466 super.onActionModeStarted(mode);
Christine Franks7452d3a2016-10-27 13:41:18 -0700467 if (mode.getTag() == CalculatorFormula.TAG_ACTION_MODE) {
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700468 mFormulaContainer.scrollTo(mFormulaText.getRight(), 0);
469 }
470 }
471
Chenjie Yu3937b652016-06-01 23:14:26 -0700472 /**
473 * Stop any active ActionMode or ContextMenu for copy/paste actions.
474 * Return true if there was one.
475 */
476 private boolean stopActionModeOrContextMenu() {
477 if (mResultText.stopActionModeOrContextMenu()) {
Hans Boehm1176f232015-05-11 16:26:03 -0700478 return true;
479 }
Chenjie Yu3937b652016-06-01 23:14:26 -0700480 if (mFormulaText.stopActionModeOrContextMenu()) {
Hans Boehm1176f232015-05-11 16:26:03 -0700481 return true;
482 }
483 return false;
484 }
485
Justin Klaassen4b3af052014-05-27 17:53:10 -0700486 @Override
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700487 public void onUserInteraction() {
488 super.onUserInteraction();
489
490 // If there's an animation in progress, end it immediately, so the user interaction can
491 // be handled.
492 if (mCurrentAnimator != null) {
493 mCurrentAnimator.end();
494 }
495 }
496
497 @Override
Justin Klaassen3b4d13d2014-06-06 18:18:37 +0100498 public void onBackPressed() {
Chenjie Yu3937b652016-06-01 23:14:26 -0700499 if (!stopActionModeOrContextMenu()) {
Annie Chin09547532016-10-14 10:59:07 -0700500 if (mDragLayout.isOpen()) {
501 // Close the layout and remove the fragment.
502 mDragLayout.setClosed();
503 getFragmentManager().popBackStack();
504 return;
505 }
Hans Boehm1176f232015-05-11 16:26:03 -0700506 if (mPadViewPager != null && mPadViewPager.getCurrentItem() != 0) {
507 // Select the previous pad.
508 mPadViewPager.setCurrentItem(mPadViewPager.getCurrentItem() - 1);
509 } else {
510 // If the user is currently looking at the first pad (or the pad is not paged),
511 // allow the system to handle the Back button.
512 super.onBackPressed();
513 }
Justin Klaassen3b4d13d2014-06-06 18:18:37 +0100514 }
515 }
516
517 @Override
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700518 public boolean onKeyUp(int keyCode, KeyEvent event) {
Justin Klaassen83959da2016-04-06 11:55:24 -0700519 // Allow the system to handle special key codes (e.g. "BACK" or "DPAD").
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700520 switch (keyCode) {
Justin Klaassen83959da2016-04-06 11:55:24 -0700521 case KeyEvent.KEYCODE_BACK:
Christine Franksf9ba2202016-10-20 17:20:19 -0700522 case KeyEvent.KEYCODE_ESCAPE:
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700523 case KeyEvent.KEYCODE_DPAD_UP:
524 case KeyEvent.KEYCODE_DPAD_DOWN:
525 case KeyEvent.KEYCODE_DPAD_LEFT:
526 case KeyEvent.KEYCODE_DPAD_RIGHT:
527 return super.onKeyUp(keyCode, event);
528 }
529
Chenjie Yu3937b652016-06-01 23:14:26 -0700530 // Stop the action mode or context menu if it's showing.
531 stopActionModeOrContextMenu();
Justin Klaassend12e0622016-04-27 16:26:47 -0700532
Hans Boehmced295e2016-11-17 17:30:13 -0800533 // Always cancel unrequested in-progress evaluation of the main expression, so that
534 // we don't have to worry about subsequent asynchronous completion.
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700535 // Requested in-progress evaluations are handled below.
536 if (mCurrentState != CalculatorState.EVALUATE) {
Hans Boehmced295e2016-11-17 17:30:13 -0800537 mEvaluator.cancel(Evaluator.MAIN_INDEX, true);
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700538 }
539
540 switch (keyCode) {
541 case KeyEvent.KEYCODE_NUMPAD_ENTER:
542 case KeyEvent.KEYCODE_ENTER:
543 case KeyEvent.KEYCODE_DPAD_CENTER:
544 mCurrentButton = mEqualButton;
545 onEquals();
546 return true;
547 case KeyEvent.KEYCODE_DEL:
548 mCurrentButton = mDeleteButton;
549 onDelete();
550 return true;
Annie Chin56bcbf12016-09-23 17:04:22 -0700551 case KeyEvent.KEYCODE_CLEAR:
552 mCurrentButton = mClearButton;
553 onClear();
554 return true;
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700555 default:
556 cancelIfEvaluating(false);
557 final int raw = event.getKeyCharacterMap().get(keyCode, event.getMetaState());
558 if ((raw & KeyCharacterMap.COMBINING_ACCENT) != 0) {
559 return true; // discard
560 }
561 // Try to discard non-printing characters and the like.
562 // The user will have to explicitly delete other junk that gets past us.
563 if (Character.isIdentifierIgnorable(raw) || Character.isWhitespace(raw)) {
564 return true;
565 }
566 char c = (char) raw;
567 if (c == '=') {
568 mCurrentButton = mEqualButton;
569 onEquals();
570 } else {
571 addChars(String.valueOf(c), true);
572 redisplayAfterFormulaChange();
573 }
574 return true;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700575 }
576 }
577
Justin Klaassene2711cb2015-05-28 11:13:17 -0700578 /**
579 * Invoked whenever the inverse button is toggled to update the UI.
580 *
581 * @param showInverse {@code true} if inverse functions should be shown
582 */
583 private void onInverseToggled(boolean showInverse) {
Justin Klaassen3e223ea2016-02-05 14:18:06 -0800584 mInverseToggle.setSelected(showInverse);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700585 if (showInverse) {
586 mInverseToggle.setContentDescription(getString(R.string.desc_inv_on));
Justin Klaassen721ec842015-05-28 14:30:08 -0700587 for (View invertibleButton : mInvertibleButtons) {
588 invertibleButton.setVisibility(View.GONE);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700589 }
590 for (View inverseButton : mInverseButtons) {
591 inverseButton.setVisibility(View.VISIBLE);
592 }
593 } else {
594 mInverseToggle.setContentDescription(getString(R.string.desc_inv_off));
Justin Klaassen721ec842015-05-28 14:30:08 -0700595 for (View invertibleButton : mInvertibleButtons) {
596 invertibleButton.setVisibility(View.VISIBLE);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700597 }
598 for (View inverseButton : mInverseButtons) {
599 inverseButton.setVisibility(View.GONE);
600 }
601 }
602 }
603
604 /**
Christine Frankseeff27f2016-07-29 12:05:29 -0700605 * Invoked whenever the deg/rad mode may have changed to update the UI. Note that the mode has
606 * not necessarily actually changed where this is invoked.
Justin Klaassene2711cb2015-05-28 11:13:17 -0700607 *
608 * @param degreeMode {@code true} if in degree mode
609 */
610 private void onModeChanged(boolean degreeMode) {
611 if (degreeMode) {
Justin Klaassend48b7562015-04-16 16:51:38 -0700612 mModeView.setText(R.string.mode_deg);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700613 mModeView.setContentDescription(getString(R.string.desc_mode_deg));
614
615 mModeToggle.setText(R.string.mode_rad);
616 mModeToggle.setContentDescription(getString(R.string.desc_switch_rad));
Hans Boehmbfe8c222015-04-02 16:26:07 -0700617 } else {
Justin Klaassend48b7562015-04-16 16:51:38 -0700618 mModeView.setText(R.string.mode_rad);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700619 mModeView.setContentDescription(getString(R.string.desc_mode_rad));
620
621 mModeToggle.setText(R.string.mode_deg);
622 mModeToggle.setContentDescription(getString(R.string.desc_switch_deg));
Hans Boehmbfe8c222015-04-02 16:26:07 -0700623 }
624 }
Hans Boehm84614952014-11-25 18:46:17 -0800625
Hans Boehm5d79d102015-09-16 16:33:47 -0700626 /**
627 * Switch to INPUT from RESULT state in response to input of the specified button_id.
628 * View.NO_ID is treated as an incomplete function id.
629 */
630 private void switchToInput(int button_id) {
631 if (KeyMaps.isBinary(button_id) || KeyMaps.isSuffix(button_id)) {
Hans Boehm8f051c32016-10-03 16:53:58 -0700632 mEvaluator.collapse(mEvaluator.getMaxIndex() /* Most recent history entry */);
Hans Boehm5d79d102015-09-16 16:33:47 -0700633 } else {
634 announceClearedForAccessibility();
Hans Boehm8f051c32016-10-03 16:53:58 -0700635 mEvaluator.clearMain();
Hans Boehm5d79d102015-09-16 16:33:47 -0700636 }
637 setState(CalculatorState.INPUT);
638 }
639
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700640 // Add the given button id to input expression.
641 // If appropriate, clear the expression before doing so.
642 private void addKeyToExpr(int id) {
643 if (mCurrentState == CalculatorState.ERROR) {
644 setState(CalculatorState.INPUT);
645 } else if (mCurrentState == CalculatorState.RESULT) {
Hans Boehm5d79d102015-09-16 16:33:47 -0700646 switchToInput(id);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700647 }
648 if (!mEvaluator.append(id)) {
649 // TODO: Some user visible feedback?
650 }
651 }
652
Hans Boehm017de982015-06-10 17:46:03 -0700653 /**
654 * Add the given button id to input expression, assuming it was explicitly
655 * typed/touched.
656 * We perform slightly more aggressive correction than in pasted expressions.
657 */
658 private void addExplicitKeyToExpr(int id) {
659 if (mCurrentState == CalculatorState.INPUT && id == R.id.op_sub) {
Hans Boehm8f051c32016-10-03 16:53:58 -0700660 mEvaluator.getExpr(Evaluator.MAIN_INDEX).removeTrailingAdditiveOperators();
Hans Boehm017de982015-06-10 17:46:03 -0700661 }
662 addKeyToExpr(id);
663 }
664
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700665 private void redisplayAfterFormulaChange() {
666 // TODO: Could do this more incrementally.
667 redisplayFormula();
668 setState(CalculatorState.INPUT);
Hans Boehm8f051c32016-10-03 16:53:58 -0700669 mResultText.clear();
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800670 if (haveUnprocessed()) {
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800671 // Force reevaluation when text is deleted, even if expression is unchanged.
672 mEvaluator.touch();
673 } else {
Hans Boehm8f051c32016-10-03 16:53:58 -0700674 if (mEvaluator.getExpr(Evaluator.MAIN_INDEX).hasInterestingOps()) {
675 mEvaluator.evaluateAndNotify(Evaluator.MAIN_INDEX, this, mResultText);
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800676 }
Hans Boehmc023b732015-04-29 11:30:47 -0700677 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700678 }
679
Hans Boehm52d477a2016-04-01 17:42:50 -0700680 /**
681 * Show the toolbar.
682 * Automatically hide it again if it's not relevant to current formula.
683 */
684 private void showAndMaybeHideToolbar() {
685 final boolean shouldBeVisible =
686 mCurrentState == CalculatorState.INPUT && mEvaluator.hasTrigFuncs();
687 mDisplayView.showToolbar(!shouldBeVisible);
688 }
689
690 /**
691 * Display or hide the toolbar depending on calculator state.
692 */
693 private void showOrHideToolbar() {
694 final boolean shouldBeVisible =
695 mCurrentState == CalculatorState.INPUT && mEvaluator.hasTrigFuncs();
696 if (shouldBeVisible) {
697 mDisplayView.showToolbar(false);
698 } else {
699 mDisplayView.hideToolbar();
700 }
701 }
702
Justin Klaassen4b3af052014-05-27 17:53:10 -0700703 public void onButtonClick(View view) {
Hans Boehmc1ea0912015-06-19 15:05:07 -0700704 // Any animation is ended before we get here.
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700705 mCurrentButton = view;
Chenjie Yu3937b652016-06-01 23:14:26 -0700706 stopActionModeOrContextMenu();
Justin Klaassen9d33cdc2016-02-21 14:16:14 -0800707
Hans Boehmc1ea0912015-06-19 15:05:07 -0700708 // See onKey above for the rationale behind some of the behavior below:
709 if (mCurrentState != CalculatorState.EVALUATE) {
Hans Boehmced295e2016-11-17 17:30:13 -0800710 // Cancel main expression evaluations that were not specifically requested.
711 mEvaluator.cancel(Evaluator.MAIN_INDEX, true);
Hans Boehm84614952014-11-25 18:46:17 -0800712 }
Justin Klaassen9d33cdc2016-02-21 14:16:14 -0800713
Justin Klaassend48b7562015-04-16 16:51:38 -0700714 final int id = view.getId();
Hans Boehm84614952014-11-25 18:46:17 -0800715 switch (id) {
Justin Klaassen4b3af052014-05-27 17:53:10 -0700716 case R.id.eq:
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700717 onEquals();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700718 break;
719 case R.id.del:
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700720 onDelete();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700721 break;
722 case R.id.clr:
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700723 onClear();
Hans Boehm52d477a2016-04-01 17:42:50 -0700724 return; // Toolbar visibility adjusted at end of animation.
Justin Klaassene2711cb2015-05-28 11:13:17 -0700725 case R.id.toggle_inv:
726 final boolean selected = !mInverseToggle.isSelected();
727 mInverseToggle.setSelected(selected);
728 onInverseToggled(selected);
Hans Boehmc1ea0912015-06-19 15:05:07 -0700729 if (mCurrentState == CalculatorState.RESULT) {
730 mResultText.redisplay(); // In case we cancelled reevaluation.
731 }
Justin Klaassene2711cb2015-05-28 11:13:17 -0700732 break;
733 case R.id.toggle_mode:
Hans Boehmc1ea0912015-06-19 15:05:07 -0700734 cancelIfEvaluating(false);
Hans Boehm8f051c32016-10-03 16:53:58 -0700735 final boolean mode = !mEvaluator.getDegreeMode(Evaluator.MAIN_INDEX);
736 if (mCurrentState == CalculatorState.RESULT
737 && mEvaluator.getExpr(Evaluator.MAIN_INDEX).hasTrigFuncs()) {
738 // Capture current result evaluated in old mode.
739 mEvaluator.collapse(mEvaluator.getMaxIndex());
Hans Boehmbfe8c222015-04-02 16:26:07 -0700740 redisplayFormula();
741 }
742 // In input mode, we reinterpret already entered trig functions.
743 mEvaluator.setDegreeMode(mode);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700744 onModeChanged(mode);
Christine Frankseeff27f2016-07-29 12:05:29 -0700745 // Show the toolbar to highlight the mode change.
746 showAndMaybeHideToolbar();
Hans Boehmbfe8c222015-04-02 16:26:07 -0700747 setState(CalculatorState.INPUT);
Justin Klaassen44595162015-05-28 17:55:20 -0700748 mResultText.clear();
Hans Boehm8f051c32016-10-03 16:53:58 -0700749 if (!haveUnprocessed()
750 && mEvaluator.getExpr(Evaluator.MAIN_INDEX).hasInterestingOps()) {
751 mEvaluator.evaluateAndNotify(mEvaluator.MAIN_INDEX, this, mResultText);
Hans Boehmc023b732015-04-29 11:30:47 -0700752 }
Christine Frankseeff27f2016-07-29 12:05:29 -0700753 return;
Hans Boehm8f051c32016-10-03 16:53:58 -0700754 case R.id.memory_store:
755 onMemoryStore();
756 return;
757 case R.id.memory_recall:
758 onMemoryRecall();
759 return;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700760 default:
Hans Boehmc1ea0912015-06-19 15:05:07 -0700761 cancelIfEvaluating(false);
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800762 if (haveUnprocessed()) {
763 // For consistency, append as uninterpreted characters.
764 // This may actually be useful for a left parenthesis.
765 addChars(KeyMaps.toString(this, id), true);
766 } else {
767 addExplicitKeyToExpr(id);
768 redisplayAfterFormulaChange();
769 }
Justin Klaassen4b3af052014-05-27 17:53:10 -0700770 break;
771 }
Hans Boehm52d477a2016-04-01 17:42:50 -0700772 showOrHideToolbar();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700773 }
774
Hans Boehm84614952014-11-25 18:46:17 -0800775 void redisplayFormula() {
Hans Boehm8f051c32016-10-03 16:53:58 -0700776 SpannableStringBuilder formula
777 = mEvaluator.getExpr(Evaluator.MAIN_INDEX).toSpannableStringBuilder(this);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700778 if (mUnprocessedChars != null) {
779 // Add and highlight characters we couldn't process.
Hans Boehm8a4f81c2015-07-09 10:41:25 -0700780 formula.append(mUnprocessedChars, mUnprocessedColorSpan,
781 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700782 }
Hans Boehm8a4f81c2015-07-09 10:41:25 -0700783 mFormulaText.changeTextTo(formula);
Annie Chinf360ef02016-03-10 13:45:39 -0800784 mFormulaText.setContentDescription(TextUtils.isEmpty(formula)
Justin Klaassend1831412016-07-19 21:59:10 -0700785 ? getString(R.string.desc_formula) : null);
Hans Boehm84614952014-11-25 18:46:17 -0800786 }
787
Justin Klaassen4b3af052014-05-27 17:53:10 -0700788 @Override
789 public boolean onLongClick(View view) {
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700790 mCurrentButton = view;
791
Justin Klaassen4b3af052014-05-27 17:53:10 -0700792 if (view.getId() == R.id.del) {
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700793 onClear();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700794 return true;
795 }
796 return false;
797 }
798
Hans Boehm84614952014-11-25 18:46:17 -0800799 // Initial evaluation completed successfully. Initiate display.
Hans Boehm8f051c32016-10-03 16:53:58 -0700800 public void onEvaluate(long index, int initDisplayPrec, int msd, int leastDigPos,
Hans Boehma0e45f32015-05-30 13:20:35 -0700801 String truncatedWholeNumber) {
Hans Boehm8f051c32016-10-03 16:53:58 -0700802 if (index != Evaluator.MAIN_INDEX) {
803 throw new AssertionError("Unexpected evaluation result index\n");
804 }
805 if (mStoreToMemoryRequested) {
806 mEvaluator.copyToMemory(Evaluator.MAIN_INDEX);
807 mStoreToMemoryRequested = false;
808 }
Justin Klaassend48b7562015-04-16 16:51:38 -0700809 // Invalidate any options that may depend on the current result.
810 invalidateOptionsMenu();
811
Hans Boehm8f051c32016-10-03 16:53:58 -0700812 mResultText.onEvaluate(index, initDisplayPrec, msd, leastDigPos, truncatedWholeNumber);
Hans Boehm61568a12015-05-18 18:25:41 -0700813 if (mCurrentState != CalculatorState.INPUT) { // in EVALUATE or INIT state
Hans Boehm84614952014-11-25 18:46:17 -0800814 onResult(mCurrentState != CalculatorState.INIT);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700815 }
Hans Boehm84614952014-11-25 18:46:17 -0800816 }
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700817
Hans Boehmc1ea0912015-06-19 15:05:07 -0700818 // Reset state to reflect evaluator cancellation. Invoked by evaluator.
Hans Boehm8f051c32016-10-03 16:53:58 -0700819 public void onCancelled(long index) {
820 // Index is Evaluator.MAIN_INDEX. We should be in EVALUATE state.
Hans Boehm84614952014-11-25 18:46:17 -0800821 setState(CalculatorState.INPUT);
Hans Boehm8f051c32016-10-03 16:53:58 -0700822 mResultText.onCancelled(index);
Hans Boehm84614952014-11-25 18:46:17 -0800823 }
824
825 // Reevaluation completed; ask result to redisplay current value.
Hans Boehm8f051c32016-10-03 16:53:58 -0700826 public void onReevaluate(long index)
Hans Boehm84614952014-11-25 18:46:17 -0800827 {
Hans Boehm8f051c32016-10-03 16:53:58 -0700828 // Index is Evaluator.MAIN_INDEX.
829 mResultText.onReevaluate(index);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700830 }
831
Justin Klaassenfed941a2014-06-09 18:42:40 +0100832 @Override
833 public void onTextSizeChanged(final TextView textView, float oldSize) {
834 if (mCurrentState != CalculatorState.INPUT) {
835 // Only animate text changes that occur from user input.
836 return;
837 }
838
839 // Calculate the values needed to perform the scale and translation animations,
840 // maintaining the same apparent baseline for the displayed text.
841 final float textScale = oldSize / textView.getTextSize();
842 final float translationX = (1.0f - textScale) *
843 (textView.getWidth() / 2.0f - textView.getPaddingEnd());
844 final float translationY = (1.0f - textScale) *
845 (textView.getHeight() / 2.0f - textView.getPaddingBottom());
846
847 final AnimatorSet animatorSet = new AnimatorSet();
848 animatorSet.playTogether(
849 ObjectAnimator.ofFloat(textView, View.SCALE_X, textScale, 1.0f),
850 ObjectAnimator.ofFloat(textView, View.SCALE_Y, textScale, 1.0f),
851 ObjectAnimator.ofFloat(textView, View.TRANSLATION_X, translationX, 0.0f),
852 ObjectAnimator.ofFloat(textView, View.TRANSLATION_Y, translationY, 0.0f));
Justin Klaassen94db7202014-06-11 11:22:31 -0700853 animatorSet.setDuration(getResources().getInteger(android.R.integer.config_mediumAnimTime));
Justin Klaassenfed941a2014-06-09 18:42:40 +0100854 animatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
855 animatorSet.start();
856 }
857
Hans Boehmc1ea0912015-06-19 15:05:07 -0700858 /**
859 * Cancel any in-progress explicitly requested evaluations.
860 * @param quiet suppress pop-up message. Explicit evaluation can change the expression
861 value, and certainly changes the display, so it seems reasonable to warn.
862 * @return true if there was such an evaluation
863 */
864 private boolean cancelIfEvaluating(boolean quiet) {
865 if (mCurrentState == CalculatorState.EVALUATE) {
Hans Boehm8f051c32016-10-03 16:53:58 -0700866 // TODO: Maybe just cancel main expression evaluation?
Hans Boehmc1ea0912015-06-19 15:05:07 -0700867 mEvaluator.cancelAll(quiet);
868 return true;
869 } else {
870 return false;
871 }
872 }
873
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800874 private boolean haveUnprocessed() {
875 return mUnprocessedChars != null && !mUnprocessedChars.isEmpty();
876 }
877
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700878 private void onEquals() {
Hans Boehm56d6e762016-06-06 11:46:29 -0700879 // Ignore if in non-INPUT state, or if there are no operators.
Justin Klaassena8075af2016-07-27 15:24:45 -0700880 if (mCurrentState == CalculatorState.INPUT) {
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800881 if (haveUnprocessed()) {
Justin Klaassena8075af2016-07-27 15:24:45 -0700882 setState(CalculatorState.EVALUATE);
Hans Boehm8f051c32016-10-03 16:53:58 -0700883 onError(Evaluator.MAIN_INDEX, R.string.error_syntax);
884 } else if (mEvaluator.getExpr(Evaluator.MAIN_INDEX).hasInterestingOps()) {
Justin Klaassena8075af2016-07-27 15:24:45 -0700885 setState(CalculatorState.EVALUATE);
Hans Boehm8f051c32016-10-03 16:53:58 -0700886 mEvaluator.requireResult(Evaluator.MAIN_INDEX, this, mResultText);
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800887 }
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700888 }
889 }
890
891 private void onDelete() {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700892 // Delete works like backspace; remove the last character or operator from the expression.
893 // Note that we handle keyboard delete exactly like the delete button. For
894 // example the delete button can be used to delete a character from an incomplete
895 // function name typed on a physical keyboard.
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700896 // This should be impossible in RESULT state.
Hans Boehmc1ea0912015-06-19 15:05:07 -0700897 // If there is an in-progress explicit evaluation, just cancel it and return.
898 if (cancelIfEvaluating(false)) return;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700899 setState(CalculatorState.INPUT);
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800900 if (haveUnprocessed()) {
901 mUnprocessedChars = mUnprocessedChars.substring(0, mUnprocessedChars.length() - 1);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700902 } else {
Hans Boehmc023b732015-04-29 11:30:47 -0700903 mEvaluator.delete();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700904 }
Hans Boehm8f051c32016-10-03 16:53:58 -0700905 if (mEvaluator.getExpr(Evaluator.MAIN_INDEX).isEmpty() && !haveUnprocessed()) {
Hans Boehmdb6f9992015-08-19 12:32:56 -0700906 // Resulting formula won't be announced, since it's empty.
907 announceClearedForAccessibility();
908 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700909 redisplayAfterFormulaChange();
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700910 }
911
Hans Boehm8f051c32016-10-03 16:53:58 -0700912 private void onMemoryStore() {
913 if (mCurrentState == CalculatorState.RESULT) {
914 mEvaluator.copyToMemory(Evaluator.MAIN_INDEX);
915 } else {
916 // Defer the store until we have the actual result.
917 mStoreToMemoryRequested = true;
918 if (mCurrentState == CalculatorState.INPUT) {
919 onEquals();
920 }
921 }
922 }
923
924 private void onMemoryRecall() {
925 clearIfNotInputState();
926 long memoryIndex = mEvaluator.getMemoryIndex();
927 if (memoryIndex != 0) {
928 mEvaluator.appendExpr(mEvaluator.getMemoryIndex());
929 redisplayAfterFormulaChange();
930 } // FIXME: Avoid the 0 case, e.g. by graying out button when memory is unavailable.
931 }
932
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700933 private void reveal(View sourceView, int colorRes, AnimatorListener listener) {
Justin Klaassen06360f92014-08-28 11:08:44 -0700934 final ViewGroupOverlay groupOverlay =
935 (ViewGroupOverlay) getWindow().getDecorView().getOverlay();
Justin Klaassen8fff1442014-06-19 10:43:29 -0700936
937 final Rect displayRect = new Rect();
Justin Klaassen06360f92014-08-28 11:08:44 -0700938 mDisplayView.getGlobalVisibleRect(displayRect);
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700939
940 // Make reveal cover the display and status bar.
941 final View revealView = new View(this);
Justin Klaassen8fff1442014-06-19 10:43:29 -0700942 revealView.setBottom(displayRect.bottom);
943 revealView.setLeft(displayRect.left);
944 revealView.setRight(displayRect.right);
Chenjie Yu3937b652016-06-01 23:14:26 -0700945 revealView.setBackgroundColor(ContextCompat.getColor(this, colorRes));
Justin Klaassen06360f92014-08-28 11:08:44 -0700946 groupOverlay.add(revealView);
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700947
Justin Klaassen4b3af052014-05-27 17:53:10 -0700948 final int[] clearLocation = new int[2];
949 sourceView.getLocationInWindow(clearLocation);
950 clearLocation[0] += sourceView.getWidth() / 2;
951 clearLocation[1] += sourceView.getHeight() / 2;
952
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700953 final int revealCenterX = clearLocation[0] - revealView.getLeft();
954 final int revealCenterY = clearLocation[1] - revealView.getTop();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700955
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700956 final double x1_2 = Math.pow(revealView.getLeft() - revealCenterX, 2);
957 final double x2_2 = Math.pow(revealView.getRight() - revealCenterX, 2);
958 final double y_2 = Math.pow(revealView.getTop() - revealCenterY, 2);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700959 final float revealRadius = (float) Math.max(Math.sqrt(x1_2 + y_2), Math.sqrt(x2_2 + y_2));
960
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700961 final Animator revealAnimator =
962 ViewAnimationUtils.createCircularReveal(revealView,
ztenghui3d6ecaf2014-06-05 09:56:00 -0700963 revealCenterX, revealCenterY, 0.0f, revealRadius);
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700964 revealAnimator.setDuration(
Justin Klaassen4b3af052014-05-27 17:53:10 -0700965 getResources().getInteger(android.R.integer.config_longAnimTime));
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700966 revealAnimator.addListener(listener);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700967
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700968 final Animator alphaAnimator = ObjectAnimator.ofFloat(revealView, View.ALPHA, 0.0f);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700969 alphaAnimator.setDuration(
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700970 getResources().getInteger(android.R.integer.config_mediumAnimTime));
Justin Klaassen4b3af052014-05-27 17:53:10 -0700971
972 final AnimatorSet animatorSet = new AnimatorSet();
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700973 animatorSet.play(revealAnimator).before(alphaAnimator);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700974 animatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
975 animatorSet.addListener(new AnimatorListenerAdapter() {
976 @Override
Justin Klaassen4b3af052014-05-27 17:53:10 -0700977 public void onAnimationEnd(Animator animator) {
Justin Klaassen8fff1442014-06-19 10:43:29 -0700978 groupOverlay.remove(revealView);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700979 mCurrentAnimator = null;
980 }
981 });
982
983 mCurrentAnimator = animatorSet;
984 animatorSet.start();
985 }
986
Hans Boehmdb6f9992015-08-19 12:32:56 -0700987 private void announceClearedForAccessibility() {
988 mResultText.announceForAccessibility(getResources().getString(R.string.cleared));
Hans Boehmccc55662015-07-07 14:16:59 -0700989 }
990
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700991 private void onClear() {
Hans Boehm8f051c32016-10-03 16:53:58 -0700992 if (mEvaluator.getExpr(Evaluator.MAIN_INDEX).isEmpty() && !haveUnprocessed()) {
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700993 return;
994 }
Hans Boehmc1ea0912015-06-19 15:05:07 -0700995 cancelIfEvaluating(true);
Hans Boehmdb6f9992015-08-19 12:32:56 -0700996 announceClearedForAccessibility();
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700997 reveal(mCurrentButton, R.color.calculator_accent_color, new AnimatorListenerAdapter() {
998 @Override
999 public void onAnimationEnd(Animator animation) {
Hans Boehm760a9dc2015-04-20 10:27:12 -07001000 mUnprocessedChars = null;
Justin Klaassen44595162015-05-28 17:55:20 -07001001 mResultText.clear();
Hans Boehm8f051c32016-10-03 16:53:58 -07001002 mEvaluator.clearMain();
Hans Boehm760a9dc2015-04-20 10:27:12 -07001003 setState(CalculatorState.INPUT);
Hans Boehm52d477a2016-04-01 17:42:50 -07001004 showOrHideToolbar();
Hans Boehm84614952014-11-25 18:46:17 -08001005 redisplayFormula();
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001006 }
1007 });
1008 }
1009
Hans Boehm84614952014-11-25 18:46:17 -08001010 // Evaluation encountered en error. Display the error.
Hans Boehm8f051c32016-10-03 16:53:58 -07001011 @Override
1012 public void onError(final long index, final int errorResourceId) {
1013 if (index != Evaluator.MAIN_INDEX) {
1014 throw new AssertionError("Unexpected error source");
1015 }
Hans Boehmfbcef702015-04-27 18:07:47 -07001016 if (mCurrentState == CalculatorState.EVALUATE) {
1017 setState(CalculatorState.ANIMATE);
Hans Boehmccc55662015-07-07 14:16:59 -07001018 mResultText.announceForAccessibility(getResources().getString(errorResourceId));
Hans Boehmfbcef702015-04-27 18:07:47 -07001019 reveal(mCurrentButton, R.color.calculator_error_color,
1020 new AnimatorListenerAdapter() {
1021 @Override
1022 public void onAnimationEnd(Animator animation) {
1023 setState(CalculatorState.ERROR);
Hans Boehm8f051c32016-10-03 16:53:58 -07001024 mResultText.onError(index, errorResourceId);
Hans Boehmfbcef702015-04-27 18:07:47 -07001025 }
1026 });
1027 } else if (mCurrentState == CalculatorState.INIT) {
1028 setState(CalculatorState.ERROR);
Hans Boehm8f051c32016-10-03 16:53:58 -07001029 mResultText.onError(index, errorResourceId);
Hans Boehmc023b732015-04-29 11:30:47 -07001030 } else {
Justin Klaassen44595162015-05-28 17:55:20 -07001031 mResultText.clear();
Justin Klaassen2be4fdb2014-08-06 19:54:09 -07001032 }
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001033 }
1034
Hans Boehm84614952014-11-25 18:46:17 -08001035 // Animate movement of result into the top formula slot.
1036 // Result window now remains translated in the top slot while the result is displayed.
1037 // (We convert it back to formula use only when the user provides new input.)
Justin Klaassen44595162015-05-28 17:55:20 -07001038 // Historical note: In the Lollipop version, this invisibly and instantaneously moved
Hans Boehm84614952014-11-25 18:46:17 -08001039 // formula and result displays back at the end of the animation. We no longer do that,
1040 // so that we can continue to properly support scrolling of the result.
1041 // We assume the result already contains the text to be expanded.
1042 private void onResult(boolean animate) {
Justin Klaassen44595162015-05-28 17:55:20 -07001043 // Calculate the textSize that would be used to display the result in the formula.
1044 // For scrollable results just use the minimum textSize to maximize the number of digits
1045 // that are visible on screen.
1046 float textSize = mFormulaText.getMinimumTextSize();
1047 if (!mResultText.isScrollable()) {
1048 textSize = mFormulaText.getVariableTextSize(mResultText.getText().toString());
1049 }
1050
1051 // Scale the result to match the calculated textSize, minimizing the jump-cut transition
1052 // when a result is reused in a subsequent expression.
1053 final float resultScale = textSize / mResultText.getTextSize();
1054
1055 // Set the result's pivot to match its gravity.
1056 mResultText.setPivotX(mResultText.getWidth() - mResultText.getPaddingRight());
1057 mResultText.setPivotY(mResultText.getHeight() - mResultText.getPaddingBottom());
1058
1059 // Calculate the necessary translations so the result takes the place of the formula and
1060 // the formula moves off the top of the screen.
Annie Chin28589dc2016-06-09 17:50:51 -07001061 final float resultTranslationY = (mFormulaContainer.getBottom() - mResultText.getBottom())
1062 - (mFormulaText.getPaddingBottom() - mResultText.getPaddingBottom());
1063 float formulaTranslationY = -mFormulaContainer.getBottom();
Annie Chin26e159e2016-05-18 15:17:14 -07001064 if (mOneLine) {
1065 // Position the result text.
1066 mResultText.setY(mResultText.getBottom());
Annie Chin28589dc2016-06-09 17:50:51 -07001067 formulaTranslationY = -(findViewById(R.id.toolbar).getBottom()
1068 + mFormulaContainer.getBottom());
Annie Chin26e159e2016-05-18 15:17:14 -07001069 }
Justin Klaassen4b3af052014-05-27 17:53:10 -07001070
Justin Klaassen44595162015-05-28 17:55:20 -07001071 // Change the result's textColor to match the formula.
1072 final int formulaTextColor = mFormulaText.getCurrentTextColor();
1073
Hans Boehm84614952014-11-25 18:46:17 -08001074 if (animate) {
Hans Boehmccc55662015-07-07 14:16:59 -07001075 mResultText.announceForAccessibility(getResources().getString(R.string.desc_eq));
1076 mResultText.announceForAccessibility(mResultText.getText());
Hans Boehmc1ea0912015-06-19 15:05:07 -07001077 setState(CalculatorState.ANIMATE);
Hans Boehm84614952014-11-25 18:46:17 -08001078 final AnimatorSet animatorSet = new AnimatorSet();
1079 animatorSet.playTogether(
Justin Klaassen44595162015-05-28 17:55:20 -07001080 ObjectAnimator.ofPropertyValuesHolder(mResultText,
1081 PropertyValuesHolder.ofFloat(View.SCALE_X, resultScale),
1082 PropertyValuesHolder.ofFloat(View.SCALE_Y, resultScale),
1083 PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, resultTranslationY)),
1084 ObjectAnimator.ofArgb(mResultText, TEXT_COLOR, formulaTextColor),
Annie Chine918fd22016-03-09 11:07:54 -08001085 ObjectAnimator.ofFloat(mFormulaContainer, View.TRANSLATION_Y,
1086 formulaTranslationY));
Justin Klaassen44595162015-05-28 17:55:20 -07001087 animatorSet.setDuration(getResources().getInteger(
1088 android.R.integer.config_longAnimTime));
Hans Boehm84614952014-11-25 18:46:17 -08001089 animatorSet.addListener(new AnimatorListenerAdapter() {
1090 @Override
Hans Boehm84614952014-11-25 18:46:17 -08001091 public void onAnimationEnd(Animator animation) {
Hans Boehm8f051c32016-10-03 16:53:58 -07001092 // Add current result to history.
1093 mEvaluator.preserve(true);
Hans Boehm84614952014-11-25 18:46:17 -08001094 setState(CalculatorState.RESULT);
1095 mCurrentAnimator = null;
1096 }
1097 });
Justin Klaassen4b3af052014-05-27 17:53:10 -07001098
Hans Boehm84614952014-11-25 18:46:17 -08001099 mCurrentAnimator = animatorSet;
1100 animatorSet.start();
Hans Boehm8f051c32016-10-03 16:53:58 -07001101 } else /* No animation desired; get there fast when restarting */ {
Justin Klaassen44595162015-05-28 17:55:20 -07001102 mResultText.setScaleX(resultScale);
1103 mResultText.setScaleY(resultScale);
1104 mResultText.setTranslationY(resultTranslationY);
1105 mResultText.setTextColor(formulaTextColor);
Annie Chine918fd22016-03-09 11:07:54 -08001106 mFormulaContainer.setTranslationY(formulaTranslationY);
Hans Boehm8f051c32016-10-03 16:53:58 -07001107 mEvaluator.represerve();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001108 setState(CalculatorState.RESULT);
Hans Boehm84614952014-11-25 18:46:17 -08001109 }
Justin Klaassen4b3af052014-05-27 17:53:10 -07001110 }
Hans Boehm84614952014-11-25 18:46:17 -08001111
1112 // Restore positions of the formula and result displays back to their original,
1113 // pre-animation state.
1114 private void restoreDisplayPositions() {
1115 // Clear result.
Justin Klaassen44595162015-05-28 17:55:20 -07001116 mResultText.setText("");
Hans Boehm84614952014-11-25 18:46:17 -08001117 // Reset all of the values modified during the animation.
Justin Klaassen44595162015-05-28 17:55:20 -07001118 mResultText.setScaleX(1.0f);
1119 mResultText.setScaleY(1.0f);
1120 mResultText.setTranslationX(0.0f);
1121 mResultText.setTranslationY(0.0f);
Annie Chine918fd22016-03-09 11:07:54 -08001122 mFormulaContainer.setTranslationY(0.0f);
Hans Boehm84614952014-11-25 18:46:17 -08001123
Hans Boehm08e8f322015-04-21 13:18:38 -07001124 mFormulaText.requestFocus();
Hans Boehm5e6a0ca2015-09-22 17:09:01 -07001125 }
1126
1127 @Override
1128 public void onClick(AlertDialogFragment fragment, int which) {
1129 if (which == DialogInterface.BUTTON_POSITIVE) {
1130 // Timeout extension request.
Hans Boehm8f051c32016-10-03 16:53:58 -07001131 mEvaluator.setLongTimeout();
Hans Boehm5e6a0ca2015-09-22 17:09:01 -07001132 }
1133 }
Hans Boehm84614952014-11-25 18:46:17 -08001134
Justin Klaassend48b7562015-04-16 16:51:38 -07001135 @Override
1136 public boolean onCreateOptionsMenu(Menu menu) {
Justin Klaassend36d63e2015-05-05 12:59:36 -07001137 super.onCreateOptionsMenu(menu);
1138
1139 getMenuInflater().inflate(R.menu.activity_calculator, menu);
Justin Klaassend48b7562015-04-16 16:51:38 -07001140 return true;
1141 }
1142
1143 @Override
1144 public boolean onPrepareOptionsMenu(Menu menu) {
Justin Klaassend36d63e2015-05-05 12:59:36 -07001145 super.onPrepareOptionsMenu(menu);
1146
1147 // Show the leading option when displaying a result.
1148 menu.findItem(R.id.menu_leading).setVisible(mCurrentState == CalculatorState.RESULT);
1149
1150 // Show the fraction option when displaying a rational result.
1151 menu.findItem(R.id.menu_fraction).setVisible(mCurrentState == CalculatorState.RESULT
Hans Boehm8f051c32016-10-03 16:53:58 -07001152 && mEvaluator.getResult(Evaluator.MAIN_INDEX).exactlyDisplayable());
Justin Klaassend36d63e2015-05-05 12:59:36 -07001153
Justin Klaassend48b7562015-04-16 16:51:38 -07001154 return true;
Hans Boehm84614952014-11-25 18:46:17 -08001155 }
1156
1157 @Override
Justin Klaassend48b7562015-04-16 16:51:38 -07001158 public boolean onOptionsItemSelected(MenuItem item) {
Hans Boehm84614952014-11-25 18:46:17 -08001159 switch (item.getItemId()) {
Annie Chinabd202f2016-10-14 14:23:45 -07001160 case R.id.menu_history:
Annie Chin09547532016-10-14 10:59:07 -07001161 showHistoryFragment(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
1162 mDragLayout.setOpen();
Annie Chinabd202f2016-10-14 14:23:45 -07001163 return true;
Justin Klaassend36d63e2015-05-05 12:59:36 -07001164 case R.id.menu_leading:
1165 displayFull();
Hans Boehm84614952014-11-25 18:46:17 -08001166 return true;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001167 case R.id.menu_fraction:
1168 displayFraction();
1169 return true;
Justin Klaassend36d63e2015-05-05 12:59:36 -07001170 case R.id.menu_licenses:
1171 startActivity(new Intent(this, Licenses.class));
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001172 return true;
Hans Boehm84614952014-11-25 18:46:17 -08001173 default:
1174 return super.onOptionsItemSelected(item);
1175 }
1176 }
1177
Annie Chin09547532016-10-14 10:59:07 -07001178 private void showHistoryFragment(int transit) {
Annie Chind0f87d22016-10-24 09:04:12 -07001179 if (!mDragLayout.isOpen()) {
1180 getFragmentManager().beginTransaction()
1181 .replace(R.id.history_frame, mHistoryFragment, HistoryFragment.TAG)
1182 .setTransition(transit)
1183 .addToBackStack(HistoryFragment.TAG)
1184 .commit();
1185 }
Annie Chin09547532016-10-14 10:59:07 -07001186 }
1187
Christine Franks7452d3a2016-10-27 13:41:18 -07001188 private void displayMessage(String title, String message) {
1189 AlertDialogFragment.showMessageDialog(this, title, message, null);
Hans Boehm84614952014-11-25 18:46:17 -08001190 }
1191
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001192 private void displayFraction() {
Hans Boehm8f051c32016-10-03 16:53:58 -07001193 UnifiedReal result = mEvaluator.getResult(Evaluator.MAIN_INDEX);
Christine Franks7452d3a2016-10-27 13:41:18 -07001194 displayMessage(getString(R.string.menu_fraction),
1195 KeyMaps.translateResult(result.toNiceString()));
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001196 }
1197
1198 // Display full result to currently evaluated precision
1199 private void displayFull() {
1200 Resources res = getResources();
Hans Boehm24c91ed2016-06-30 18:53:44 -07001201 String msg = mResultText.getFullText(true /* withSeparators */) + " ";
Justin Klaassen44595162015-05-28 17:55:20 -07001202 if (mResultText.fullTextIsExact()) {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001203 msg += res.getString(R.string.exact);
1204 } else {
1205 msg += res.getString(R.string.approximate);
1206 }
Christine Franks7452d3a2016-10-27 13:41:18 -07001207 displayMessage(getString(R.string.menu_leading), msg);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001208 }
1209
Hans Boehm017de982015-06-10 17:46:03 -07001210 /**
1211 * Add input characters to the end of the expression.
1212 * Map them to the appropriate button pushes when possible. Leftover characters
1213 * are added to mUnprocessedChars, which is presumed to immediately precede the newly
1214 * added characters.
Hans Boehm65a99a42016-02-03 18:16:07 -08001215 * @param moreChars characters to be added
1216 * @param explicit these characters were explicitly typed by the user, not pasted
Hans Boehm017de982015-06-10 17:46:03 -07001217 */
1218 private void addChars(String moreChars, boolean explicit) {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001219 if (mUnprocessedChars != null) {
1220 moreChars = mUnprocessedChars + moreChars;
1221 }
1222 int current = 0;
1223 int len = moreChars.length();
Hans Boehm0b9806f2015-06-29 16:07:15 -07001224 boolean lastWasDigit = false;
Hans Boehm5d79d102015-09-16 16:33:47 -07001225 if (mCurrentState == CalculatorState.RESULT && len != 0) {
1226 // Clear display immediately for incomplete function name.
1227 switchToInput(KeyMaps.keyForChar(moreChars.charAt(current)));
1228 }
Hans Boehm24c91ed2016-06-30 18:53:44 -07001229 char groupingSeparator = KeyMaps.translateResult(",").charAt(0);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001230 while (current < len) {
1231 char c = moreChars.charAt(current);
Hans Boehm24c91ed2016-06-30 18:53:44 -07001232 if (Character.isSpaceChar(c) || c == groupingSeparator) {
1233 ++current;
1234 continue;
1235 }
Hans Boehm013969e2015-04-13 20:29:47 -07001236 int k = KeyMaps.keyForChar(c);
Hans Boehm0b9806f2015-06-29 16:07:15 -07001237 if (!explicit) {
1238 int expEnd;
1239 if (lastWasDigit && current !=
1240 (expEnd = Evaluator.exponentEnd(moreChars, current))) {
1241 // Process scientific notation with 'E' when pasting, in spite of ambiguity
1242 // with base of natural log.
1243 // Otherwise the 10^x key is the user's friend.
1244 mEvaluator.addExponent(moreChars, current, expEnd);
1245 current = expEnd;
1246 lastWasDigit = false;
1247 continue;
1248 } else {
1249 boolean isDigit = KeyMaps.digVal(k) != KeyMaps.NOT_DIGIT;
1250 if (current == 0 && (isDigit || k == R.id.dec_point)
Hans Boehm8f051c32016-10-03 16:53:58 -07001251 && mEvaluator.getExpr(Evaluator.MAIN_INDEX).hasTrailingConstant()) {
Hans Boehm0b9806f2015-06-29 16:07:15 -07001252 // Refuse to concatenate pasted content to trailing constant.
1253 // This makes pasting of calculator results more consistent, whether or
1254 // not the old calculator instance is still around.
1255 addKeyToExpr(R.id.op_mul);
1256 }
1257 lastWasDigit = (isDigit || lastWasDigit && k == R.id.dec_point);
1258 }
1259 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001260 if (k != View.NO_ID) {
1261 mCurrentButton = findViewById(k);
Hans Boehm017de982015-06-10 17:46:03 -07001262 if (explicit) {
1263 addExplicitKeyToExpr(k);
1264 } else {
1265 addKeyToExpr(k);
1266 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001267 if (Character.isSurrogate(c)) {
1268 current += 2;
1269 } else {
1270 ++current;
1271 }
1272 continue;
1273 }
Hans Boehm013969e2015-04-13 20:29:47 -07001274 int f = KeyMaps.funForString(moreChars, current);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001275 if (f != View.NO_ID) {
1276 mCurrentButton = findViewById(f);
Hans Boehm017de982015-06-10 17:46:03 -07001277 if (explicit) {
1278 addExplicitKeyToExpr(f);
1279 } else {
1280 addKeyToExpr(f);
1281 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001282 if (f == R.id.op_sqrt) {
1283 // Square root entered as function; don't lose the parenthesis.
1284 addKeyToExpr(R.id.lparen);
1285 }
1286 current = moreChars.indexOf('(', current) + 1;
1287 continue;
1288 }
1289 // There are characters left, but we can't convert them to button presses.
1290 mUnprocessedChars = moreChars.substring(current);
1291 redisplayAfterFormulaChange();
Hans Boehm52d477a2016-04-01 17:42:50 -07001292 showOrHideToolbar();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001293 return;
1294 }
1295 mUnprocessedChars = null;
1296 redisplayAfterFormulaChange();
Hans Boehm52d477a2016-04-01 17:42:50 -07001297 showOrHideToolbar();
Hans Boehm84614952014-11-25 18:46:17 -08001298 }
1299
Hans Boehm8f051c32016-10-03 16:53:58 -07001300 private void clearIfNotInputState() {
1301 if (mCurrentState == CalculatorState.ERROR
1302 || mCurrentState == CalculatorState.RESULT) {
1303 setState(CalculatorState.INPUT);
1304 mEvaluator.clearMain();
1305 }
1306 }
1307
Annie Chind0f87d22016-10-24 09:04:12 -07001308 private boolean isViewTarget(View view, MotionEvent event) {
1309 mHitRect.set(0, 0, view.getWidth(), view.getHeight());
1310 mDragLayout.offsetDescendantRectToMyCoords(view, mHitRect);
1311 return mHitRect.contains((int) event.getX(), (int) event.getY());
1312 }
1313
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001314 @Override
Justin Klaassenfc5ac822015-06-18 13:15:17 -07001315 public boolean onPaste(ClipData clip) {
1316 final ClipData.Item item = clip.getItemCount() == 0 ? null : clip.getItemAt(0);
1317 if (item == null) {
1318 // nothing to paste, bail early...
1319 return false;
1320 }
1321
1322 // Check if the item is a previously copied result, otherwise paste as raw text.
1323 final Uri uri = item.getUri();
1324 if (uri != null && mEvaluator.isLastSaved(uri)) {
Hans Boehm8f051c32016-10-03 16:53:58 -07001325 clearIfNotInputState();
1326 mEvaluator.appendExpr(mEvaluator.getSavedIndex());
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001327 redisplayAfterFormulaChange();
Justin Klaassenfc5ac822015-06-18 13:15:17 -07001328 } else {
Hans Boehm017de982015-06-10 17:46:03 -07001329 addChars(item.coerceToText(this).toString(), false);
Hans Boehm84614952014-11-25 18:46:17 -08001330 }
Justin Klaassenfc5ac822015-06-18 13:15:17 -07001331 return true;
Hans Boehm84614952014-11-25 18:46:17 -08001332 }
Chenjie Yu3937b652016-06-01 23:14:26 -07001333
1334 /**
1335 * Clean up animation for context menu.
1336 */
1337 @Override
1338 public void onContextMenuClosed(Menu menu) {
1339 stopActionModeOrContextMenu();
1340 }
Justin Klaassen4b3af052014-05-27 17:53:10 -07001341}