blob: b877ed6024d92b13b38ac2f5a1ff76c9c89df468 [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;
Christine Franksbd90b792016-11-22 10:28:26 -080082import java.text.DecimalFormatSymbols;
Justin Klaassen4b3af052014-05-27 17:53:10 -070083
Christine Franks1d99be12016-11-14 14:00:36 -080084import static com.android.calculator2.CalculatorFormula.OnFormulaContextMenuClickListener;
85
Hans Boehm8f051c32016-10-03 16:53:58 -070086public class Calculator extends Activity
Christine Franks1d99be12016-11-14 14:00:36 -080087 implements OnTextSizeChangeListener, OnLongClickListener,
Hans Boehm8f051c32016-10-03 16:53:58 -070088 AlertDialogFragment.OnClickListener, Evaluator.EvaluationListener /* for main result */ {
Justin Klaassen2be4fdb2014-08-06 19:54:09 -070089
Annie Chin9a211132016-11-30 12:52:06 -080090 private static final String TAG = "Calculator";
Justin Klaassen2be4fdb2014-08-06 19:54:09 -070091 /**
92 * Constant for an invalid resource id.
93 */
94 public static final int INVALID_RES_ID = -1;
Justin Klaassen4b3af052014-05-27 17:53:10 -070095
96 private enum CalculatorState {
Hans Boehm84614952014-11-25 18:46:17 -080097 INPUT, // Result and formula both visible, no evaluation requested,
98 // Though result may be visible on bottom line.
99 EVALUATE, // Both visible, evaluation requested, evaluation/animation incomplete.
Hans Boehmc1ea0912015-06-19 15:05:07 -0700100 // Not used for instant result evaluation.
Hans Boehm84614952014-11-25 18:46:17 -0800101 INIT, // Very temporary state used as alternative to EVALUATE
102 // during reinitialization. Do not animate on completion.
Hans Boehm31ea2522016-11-23 17:47:02 -0800103 INIT_FOR_RESULT, // Identical to INIT, but evaluation is known to terminate.
Hans Boehm84614952014-11-25 18:46:17 -0800104 ANIMATE, // Result computed, animation to enlarge result window in progress.
105 RESULT, // Result displayed, formula invisible.
106 // If we are in RESULT state, the formula was evaluated without
107 // error to initial precision.
Hans Boehm8f051c32016-10-03 16:53:58 -0700108 // The current formula is now also the last history entry.
Hans Boehm84614952014-11-25 18:46:17 -0800109 ERROR // Error displayed: Formula visible, result shows error message.
110 // Display similar to INPUT state.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700111 }
Hans Boehm84614952014-11-25 18:46:17 -0800112 // Normal transition sequence is
113 // INPUT -> EVALUATE -> ANIMATE -> RESULT (or ERROR) -> INPUT
114 // A RESULT -> ERROR transition is possible in rare corner cases, in which
115 // a higher precision evaluation exposes an error. This is possible, since we
116 // initially evaluate assuming we were given a well-defined problem. If we
117 // were actually asked to compute sqrt(<extremely tiny negative number>) we produce 0
118 // unless we are asked for enough precision that we can distinguish the argument from zero.
Hans Boehm84614952014-11-25 18:46:17 -0800119 // ANIMATE, ERROR, and RESULT are translated to an INIT state if the application
120 // is restarted in that state. This leads us to recompute and redisplay the result
121 // ASAP.
122 // TODO: Possibly save a bit more information, e.g. its initial display string
123 // or most significant digit position, to speed up restart.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700124
Justin Klaassen44595162015-05-28 17:55:20 -0700125 private final Property<TextView, Integer> TEXT_COLOR =
126 new Property<TextView, Integer>(Integer.class, "textColor") {
127 @Override
128 public Integer get(TextView textView) {
129 return textView.getCurrentTextColor();
130 }
131
132 @Override
133 public void set(TextView textView, Integer textColor) {
134 textView.setTextColor(textColor);
135 }
136 };
137
Justin Klaassen3e223ea2016-02-05 14:18:06 -0800138 private static final String NAME = "Calculator";
Hans Boehm84614952014-11-25 18:46:17 -0800139 private static final String KEY_DISPLAY_STATE = NAME + "_display_state";
Hans Boehm760a9dc2015-04-20 10:27:12 -0700140 private static final String KEY_UNPROCESSED_CHARS = NAME + "_unprocessed_chars";
Justin Klaassen3e223ea2016-02-05 14:18:06 -0800141 /**
142 * Associated value is a byte array holding the evaluator state.
143 */
Hans Boehm84614952014-11-25 18:46:17 -0800144 private static final String KEY_EVAL_STATE = NAME + "_eval_state";
Justin Klaassen3e223ea2016-02-05 14:18:06 -0800145 private static final String KEY_INVERSE_MODE = NAME + "_inverse_mode";
Christine Frankseeff27f2016-07-29 12:05:29 -0700146 /**
147 * Associated value is an boolean holding the visibility state of the toolbar.
148 */
149 private static final String KEY_SHOW_TOOLBAR = NAME + "_show_toolbar";
Justin Klaassen741471e2014-06-11 09:43:44 -0700150
Annie Chine918fd22016-03-09 11:07:54 -0800151 private final ViewTreeObserver.OnPreDrawListener mPreDrawListener =
152 new ViewTreeObserver.OnPreDrawListener() {
153 @Override
154 public boolean onPreDraw() {
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700155 mFormulaContainer.scrollTo(mFormulaText.getRight(), 0);
Annie Chine918fd22016-03-09 11:07:54 -0800156 final ViewTreeObserver observer = mFormulaContainer.getViewTreeObserver();
157 if (observer.isAlive()) {
158 observer.removeOnPreDrawListener(this);
159 }
160 return false;
161 }
162 };
163
Christine Franks1d99be12016-11-14 14:00:36 -0800164 public final OnDisplayMemoryOperationsListener mOnDisplayMemoryOperationsListener =
165 new OnDisplayMemoryOperationsListener() {
166 @Override
167 public boolean shouldDisplayMemory() {
168 return mEvaluator.getMemoryIndex() != 0;
169 }
170 };
171
172 public final OnFormulaContextMenuClickListener mOnFormulaContextMenuClickListener =
173 new OnFormulaContextMenuClickListener() {
174 @Override
175 public boolean onPaste(ClipData clip) {
176 final ClipData.Item item = clip.getItemCount() == 0 ? null : clip.getItemAt(0);
177 if (item == null) {
178 // nothing to paste, bail early...
179 return false;
180 }
181
182 // Check if the item is a previously copied result, otherwise paste as raw text.
183 final Uri uri = item.getUri();
184 if (uri != null && mEvaluator.isLastSaved(uri)) {
185 clearIfNotInputState();
186 mEvaluator.appendExpr(mEvaluator.getSavedIndex());
187 redisplayAfterFormulaChange();
188 } else {
189 addChars(item.coerceToText(Calculator.this).toString(), false);
190 }
191 return true;
192 }
193
194 @Override
195 public void onMemoryRecall() {
196 clearIfNotInputState();
197 long memoryIndex = mEvaluator.getMemoryIndex();
198 if (memoryIndex != 0) {
199 mEvaluator.appendExpr(mEvaluator.getMemoryIndex());
200 redisplayAfterFormulaChange();
201 } // FIXME: Avoid the 0 case, e.g. by graying out button when memory is unavailable.
202 }
203 };
204
205
Annie Chine918fd22016-03-09 11:07:54 -0800206 private final TextWatcher mFormulaTextWatcher = new TextWatcher() {
207 @Override
208 public void beforeTextChanged(CharSequence charSequence, int start, int count, int after) {
209 }
210
211 @Override
212 public void onTextChanged(CharSequence charSequence, int start, int count, int after) {
213 }
214
215 @Override
216 public void afterTextChanged(Editable editable) {
217 final ViewTreeObserver observer = mFormulaContainer.getViewTreeObserver();
218 if (observer.isAlive()) {
219 observer.removeOnPreDrawListener(mPreDrawListener);
220 observer.addOnPreDrawListener(mPreDrawListener);
221 }
222 }
223 };
224
Annie Chin9a211132016-11-30 12:52:06 -0800225 private final DragLayout.CloseCallback mCloseCallback = new DragLayout.CloseCallback() {
226 @Override
227 public void onClose() {
228 popFragmentBackstack();
229 }
230 };
231
Annie Chind0f87d22016-10-24 09:04:12 -0700232 private final DragLayout.DragCallback mDragCallback = new DragLayout.DragCallback() {
233 @Override
Annie Chin9a211132016-11-30 12:52:06 -0800234 public void onStartDraggingOpen() {
Annie Chind0f87d22016-10-24 09:04:12 -0700235 showHistoryFragment(FragmentTransaction.TRANSIT_NONE);
236 }
237
238 @Override
239 public void whileDragging(float yFraction) {
240 // no-op
241 }
242
243 @Override
Annie Chind0f87d22016-10-24 09:04:12 -0700244 public boolean allowDrag(MotionEvent event) {
245 return isViewTarget(mHistoryFrame, event) || isViewTarget(mDisplayView, event);
246 }
247
248 @Override
249 public boolean shouldInterceptTouchEvent(MotionEvent event) {
250 return isViewTarget(mHistoryFrame, event) || isViewTarget(mDisplayView, event);
251 }
252
253 @Override
254 public int getDisplayHeight() {
255 return mDisplayView.getMeasuredHeight();
256 }
257
258 public void onLayout(int translation) {
259 mHistoryFrame.setTranslationY(translation + mDisplayView.getBottom());
260 }
261 };
262
263 private final Rect mHitRect = new Rect();
264
Justin Klaassen4b3af052014-05-27 17:53:10 -0700265 private CalculatorState mCurrentState;
Hans Boehm84614952014-11-25 18:46:17 -0800266 private Evaluator mEvaluator;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700267
Justin Klaassen9d33cdc2016-02-21 14:16:14 -0800268 private CalculatorDisplay mDisplayView;
Justin Klaassend48b7562015-04-16 16:51:38 -0700269 private TextView mModeView;
Christine Franks7452d3a2016-10-27 13:41:18 -0700270 private CalculatorFormula mFormulaText;
Justin Klaassen44595162015-05-28 17:55:20 -0700271 private CalculatorResult mResultText;
Annie Chine918fd22016-03-09 11:07:54 -0800272 private HorizontalScrollView mFormulaContainer;
Annie Chin09547532016-10-14 10:59:07 -0700273 private DragLayout mDragLayout;
Annie Chind0f87d22016-10-24 09:04:12 -0700274 private FrameLayout mHistoryFrame;
Justin Klaassend48b7562015-04-16 16:51:38 -0700275
Justin Klaassen3b4d13d2014-06-06 18:18:37 +0100276 private ViewPager mPadViewPager;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700277 private View mDeleteButton;
278 private View mClearButton;
Justin Klaassend48b7562015-04-16 16:51:38 -0700279 private View mEqualButton;
Justin Klaassene2711cb2015-05-28 11:13:17 -0700280
281 private TextView mInverseToggle;
282 private TextView mModeToggle;
283
Justin Klaassen721ec842015-05-28 14:30:08 -0700284 private View[] mInvertibleButtons;
Justin Klaassene2711cb2015-05-28 11:13:17 -0700285 private View[] mInverseButtons;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700286
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700287 private View mCurrentButton;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700288 private Animator mCurrentAnimator;
289
Hans Boehm8a4f81c2015-07-09 10:41:25 -0700290 // Characters that were recently entered at the end of the display that have not yet
291 // been added to the underlying expression.
292 private String mUnprocessedChars = null;
293
294 // Color to highlight unprocessed characters from physical keyboard.
295 // TODO: should probably match this to the error color?
296 private ForegroundColorSpan mUnprocessedColorSpan = new ForegroundColorSpan(Color.RED);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700297
Annie Chin26e159e2016-05-18 15:17:14 -0700298 // Whether the display is one line.
299 private boolean mOneLine;
300
Annie Chin09547532016-10-14 10:59:07 -0700301 private HistoryFragment mHistoryFragment = new HistoryFragment();
302
Hans Boehm31ea2522016-11-23 17:47:02 -0800303 /**
304 * Restore Evaluator state and mCurrentState from savedInstanceState.
305 * Return true if the toolbar should be visible.
306 */
307 private void restoreInstanceState(Bundle savedInstanceState) {
308 final CalculatorState savedState = CalculatorState.values()[
309 savedInstanceState.getInt(KEY_DISPLAY_STATE,
310 CalculatorState.INPUT.ordinal())];
311 setState(savedState);
312 CharSequence unprocessed = savedInstanceState.getCharSequence(KEY_UNPROCESSED_CHARS);
313 if (unprocessed != null) {
314 mUnprocessedChars = unprocessed.toString();
315 }
316 byte[] state = savedInstanceState.getByteArray(KEY_EVAL_STATE);
317 if (state != null) {
318 try (ObjectInput in = new ObjectInputStream(new ByteArrayInputStream(state))) {
319 mEvaluator.restoreInstanceState(in);
320 } catch (Throwable ignored) {
321 // When in doubt, revert to clean state
322 mCurrentState = CalculatorState.INPUT;
323 mEvaluator.clearMain();
324 }
325 }
326 if (savedInstanceState.getBoolean(KEY_SHOW_TOOLBAR, true)) {
327 showAndMaybeHideToolbar();
328 } else {
329 mDisplayView.hideToolbar();
330 }
331 onInverseToggled(savedInstanceState.getBoolean(KEY_INVERSE_MODE));
332 // TODO: We're currently not saving and restoring scroll position.
333 // We probably should. Details may require care to deal with:
334 // - new display size
335 // - slow recomputation if we've scrolled far.
336 }
337
338 private void restoreDisplay() {
339 onModeChanged(mEvaluator.getDegreeMode(Evaluator.MAIN_INDEX));
340 if (mCurrentState != CalculatorState.RESULT
341 && mCurrentState != CalculatorState.INIT_FOR_RESULT) {
342 redisplayFormula();
343 }
344 if (mCurrentState == CalculatorState.INPUT) {
345 // This resultText will explicitly call evaluateAndNotify when ready.
346 mResultText.setShouldEvaluateResult(CalculatorResult.SHOULD_EVALUATE, this);
347 } else {
348 // Just reevaluate.
349 setState((mCurrentState == CalculatorState.RESULT
350 || mCurrentState == CalculatorState.INIT_FOR_RESULT) ?
351 CalculatorState.INIT_FOR_RESULT : CalculatorState.INIT);
352 // Request evaluation when we know display width.
353 mResultText.setShouldEvaluateResult(CalculatorResult.SHOULD_REQUIRE, this);
354 }
355 }
356
Justin Klaassen4b3af052014-05-27 17:53:10 -0700357 @Override
358 protected void onCreate(Bundle savedInstanceState) {
359 super.onCreate(savedInstanceState);
Hans Boehm31ea2522016-11-23 17:47:02 -0800360
Annie Chin09547532016-10-14 10:59:07 -0700361 setContentView(R.layout.activity_calculator_main);
Justin Klaassend48b7562015-04-16 16:51:38 -0700362 setActionBar((Toolbar) findViewById(R.id.toolbar));
363
364 // Hide all default options in the ActionBar.
365 getActionBar().setDisplayOptions(0);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700366
Justin Klaassen9d33cdc2016-02-21 14:16:14 -0800367 // Ensure the toolbar stays visible while the options menu is displayed.
368 getActionBar().addOnMenuVisibilityListener(new ActionBar.OnMenuVisibilityListener() {
369 @Override
370 public void onMenuVisibilityChanged(boolean isVisible) {
371 mDisplayView.setForceToolbarVisible(isVisible);
372 }
373 });
374
375 mDisplayView = (CalculatorDisplay) findViewById(R.id.display);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700376 mModeView = (TextView) findViewById(R.id.mode);
Christine Franks7452d3a2016-10-27 13:41:18 -0700377 mFormulaText = (CalculatorFormula) findViewById(R.id.formula);
Justin Klaassen44595162015-05-28 17:55:20 -0700378 mResultText = (CalculatorResult) findViewById(R.id.result);
Annie Chine918fd22016-03-09 11:07:54 -0800379 mFormulaContainer = (HorizontalScrollView) findViewById(R.id.formula_container);
Hans Boehm31ea2522016-11-23 17:47:02 -0800380 mEvaluator = Evaluator.getInstance(this);
381 mResultText.setEvaluator(mEvaluator, Evaluator.MAIN_INDEX);
382 KeyMaps.setActivity(this);
Justin Klaassend48b7562015-04-16 16:51:38 -0700383
Justin Klaassen3b4d13d2014-06-06 18:18:37 +0100384 mPadViewPager = (ViewPager) findViewById(R.id.pad_pager);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700385 mDeleteButton = findViewById(R.id.del);
386 mClearButton = findViewById(R.id.clr);
Christine Franksbd90b792016-11-22 10:28:26 -0800387 final View numberPad = findViewById(R.id.pad_numeric);
388 mEqualButton = numberPad.findViewById(R.id.eq);
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700389 if (mEqualButton == null || mEqualButton.getVisibility() != View.VISIBLE) {
390 mEqualButton = findViewById(R.id.pad_operator).findViewById(R.id.eq);
391 }
Christine Franksbd90b792016-11-22 10:28:26 -0800392 final TextView decimalPointButton = (TextView) numberPad.findViewById(R.id.dec_point);
393 decimalPointButton.setText(getDecimalSeparator());
Justin Klaassene2711cb2015-05-28 11:13:17 -0700394
395 mInverseToggle = (TextView) findViewById(R.id.toggle_inv);
396 mModeToggle = (TextView) findViewById(R.id.toggle_mode);
397
Annie Chin26e159e2016-05-18 15:17:14 -0700398 mOneLine = mResultText.getVisibility() == View.INVISIBLE;
399
Justin Klaassen721ec842015-05-28 14:30:08 -0700400 mInvertibleButtons = new View[] {
401 findViewById(R.id.fun_sin),
402 findViewById(R.id.fun_cos),
Hans Boehm4db31b42015-05-31 12:19:05 -0700403 findViewById(R.id.fun_tan),
404 findViewById(R.id.fun_ln),
405 findViewById(R.id.fun_log),
406 findViewById(R.id.op_sqrt)
Justin Klaassene2711cb2015-05-28 11:13:17 -0700407 };
408 mInverseButtons = new View[] {
409 findViewById(R.id.fun_arcsin),
410 findViewById(R.id.fun_arccos),
Hans Boehm4db31b42015-05-31 12:19:05 -0700411 findViewById(R.id.fun_arctan),
412 findViewById(R.id.fun_exp),
413 findViewById(R.id.fun_10pow),
414 findViewById(R.id.op_sqr)
Justin Klaassene2711cb2015-05-28 11:13:17 -0700415 };
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700416
Annie Chin09547532016-10-14 10:59:07 -0700417 mDragLayout = (DragLayout) findViewById(R.id.drag_layout);
Annie Chind0f87d22016-10-24 09:04:12 -0700418 mDragLayout.removeDragCallback(mDragCallback);
419 mDragLayout.addDragCallback(mDragCallback);
Annie Chin9a211132016-11-30 12:52:06 -0800420 mDragLayout.setCloseCallback(mCloseCallback);
Annie Chin09547532016-10-14 10:59:07 -0700421
Annie Chind0f87d22016-10-24 09:04:12 -0700422 mHistoryFrame = (FrameLayout) findViewById(R.id.history_frame);
Annie Chin09547532016-10-14 10:59:07 -0700423
Christine Franks1d99be12016-11-14 14:00:36 -0800424 mFormulaText.setOnContextMenuClickListener(mOnFormulaContextMenuClickListener);
425 mFormulaText.setOnDisplayMemoryOperationsListener(mOnDisplayMemoryOperationsListener);
426
Hans Boehm08e8f322015-04-21 13:18:38 -0700427 mFormulaText.setOnTextSizeChangeListener(this);
Annie Chine918fd22016-03-09 11:07:54 -0800428 mFormulaText.addTextChangedListener(mFormulaTextWatcher);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700429 mDeleteButton.setOnLongClickListener(this);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700430
Hans Boehm31ea2522016-11-23 17:47:02 -0800431 if (savedInstanceState != null) {
432 restoreInstanceState(savedInstanceState);
Christine Frankseeff27f2016-07-29 12:05:29 -0700433 } else {
Hans Boehm31ea2522016-11-23 17:47:02 -0800434 mCurrentState = CalculatorState.INPUT;
435 mEvaluator.clearMain();
Christine Frankseeff27f2016-07-29 12:05:29 -0700436 showAndMaybeHideToolbar();
Hans Boehm31ea2522016-11-23 17:47:02 -0800437 onInverseToggled(false);
Christine Frankseeff27f2016-07-29 12:05:29 -0700438 }
Hans Boehm31ea2522016-11-23 17:47:02 -0800439 restoreDisplay();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700440 }
441
442 @Override
Justin Klaassen9d33cdc2016-02-21 14:16:14 -0800443 protected void onResume() {
444 super.onResume();
Christine Frankseeff27f2016-07-29 12:05:29 -0700445 if (mDisplayView.isToolbarVisible()) {
446 showAndMaybeHideToolbar();
447 }
Justin Klaassen9d33cdc2016-02-21 14:16:14 -0800448 }
449
450 @Override
Justin Klaassenf79d6f62014-08-26 12:27:08 -0700451 protected void onSaveInstanceState(@NonNull Bundle outState) {
Hans Boehm40125442016-01-22 10:35:35 -0800452 mEvaluator.cancelAll(true);
Justin Klaassenf79d6f62014-08-26 12:27:08 -0700453 // If there's an animation in progress, cancel it first to ensure our state is up-to-date.
454 if (mCurrentAnimator != null) {
455 mCurrentAnimator.cancel();
456 }
457
Justin Klaassen4b3af052014-05-27 17:53:10 -0700458 super.onSaveInstanceState(outState);
Hans Boehm84614952014-11-25 18:46:17 -0800459 outState.putInt(KEY_DISPLAY_STATE, mCurrentState.ordinal());
Hans Boehm760a9dc2015-04-20 10:27:12 -0700460 outState.putCharSequence(KEY_UNPROCESSED_CHARS, mUnprocessedChars);
Hans Boehm84614952014-11-25 18:46:17 -0800461 ByteArrayOutputStream byteArrayStream = new ByteArrayOutputStream();
462 try (ObjectOutput out = new ObjectOutputStream(byteArrayStream)) {
463 mEvaluator.saveInstanceState(out);
464 } catch (IOException e) {
465 // Impossible; No IO involved.
466 throw new AssertionError("Impossible IO exception", e);
467 }
468 outState.putByteArray(KEY_EVAL_STATE, byteArrayStream.toByteArray());
Justin Klaassen3e223ea2016-02-05 14:18:06 -0800469 outState.putBoolean(KEY_INVERSE_MODE, mInverseToggle.isSelected());
Christine Frankseeff27f2016-07-29 12:05:29 -0700470 outState.putBoolean(KEY_SHOW_TOOLBAR, mDisplayView.isToolbarVisible());
Justin Klaassen4b3af052014-05-27 17:53:10 -0700471 }
472
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700473 // Set the state, updating delete label and display colors.
474 // This restores display positions on moving to INPUT.
Justin Klaassend48b7562015-04-16 16:51:38 -0700475 // But movement/animation for moving to RESULT has already been done.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700476 private void setState(CalculatorState state) {
477 if (mCurrentState != state) {
Hans Boehm84614952014-11-25 18:46:17 -0800478 if (state == CalculatorState.INPUT) {
Hans Boehmd4959e82016-11-15 18:01:28 -0800479 // We'll explicitly request evaluation from now on.
Hans Boehmbd01e4b2016-11-23 10:12:58 -0800480 mResultText.setShouldEvaluateResult(CalculatorResult.SHOULD_NOT_EVALUATE, null);
Hans Boehm84614952014-11-25 18:46:17 -0800481 restoreDisplayPositions();
482 }
Justin Klaassen4b3af052014-05-27 17:53:10 -0700483 mCurrentState = state;
484
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700485 if (mCurrentState == CalculatorState.RESULT) {
486 // No longer do this for ERROR; allow mistakes to be corrected.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700487 mDeleteButton.setVisibility(View.GONE);
488 mClearButton.setVisibility(View.VISIBLE);
489 } else {
490 mDeleteButton.setVisibility(View.VISIBLE);
491 mClearButton.setVisibility(View.GONE);
492 }
493
Annie Chin26e159e2016-05-18 15:17:14 -0700494 if (mOneLine) {
495 if (mCurrentState == CalculatorState.RESULT
496 || mCurrentState == CalculatorState.EVALUATE
497 || mCurrentState == CalculatorState.ANIMATE) {
498 mFormulaText.setVisibility(View.VISIBLE);
499 mResultText.setVisibility(View.VISIBLE);
Annie Chin947d93b2016-06-14 10:18:54 -0700500 } else if (mCurrentState == CalculatorState.ERROR) {
501 mFormulaText.setVisibility(View.INVISIBLE);
502 mResultText.setVisibility(View.VISIBLE);
Annie Chin26e159e2016-05-18 15:17:14 -0700503 } else {
504 mFormulaText.setVisibility(View.VISIBLE);
505 mResultText.setVisibility(View.INVISIBLE);
506 }
507 }
508
Hans Boehm84614952014-11-25 18:46:17 -0800509 if (mCurrentState == CalculatorState.ERROR) {
Chenjie Yu3937b652016-06-01 23:14:26 -0700510 final int errorColor =
511 ContextCompat.getColor(this, R.color.calculator_error_color);
Hans Boehm08e8f322015-04-21 13:18:38 -0700512 mFormulaText.setTextColor(errorColor);
Justin Klaassen44595162015-05-28 17:55:20 -0700513 mResultText.setTextColor(errorColor);
Justin Klaassen8fff1442014-06-19 10:43:29 -0700514 getWindow().setStatusBarColor(errorColor);
Justin Klaassen44595162015-05-28 17:55:20 -0700515 } else if (mCurrentState != CalculatorState.RESULT) {
Chenjie Yu3937b652016-06-01 23:14:26 -0700516 mFormulaText.setTextColor(
517 ContextCompat.getColor(this, R.color.display_formula_text_color));
518 mResultText.setTextColor(
519 ContextCompat.getColor(this, R.color.display_result_text_color));
520 getWindow().setStatusBarColor(
521 ContextCompat.getColor(this, R.color.calculator_accent_color));
Justin Klaassen4b3af052014-05-27 17:53:10 -0700522 }
Justin Klaassend48b7562015-04-16 16:51:38 -0700523
524 invalidateOptionsMenu();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700525 }
526 }
527
Annie Chin94c1bd92016-11-23 13:39:56 -0800528 public boolean isResultLayout() {
Hans Boehm31ea2522016-11-23 17:47:02 -0800529 if (mCurrentState == CalculatorState.ANIMATE) {
530 throw new AssertionError("impossible state");
531 }
532 // Note that ERROR has INPUT, not RESULT layout.
533 return mCurrentState == CalculatorState.INIT_FOR_RESULT
534 || mCurrentState == CalculatorState.RESULT;
Annie Chin70ac8ea2016-11-18 14:43:56 -0800535 }
536
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700537 @Override
Annie Chind0f87d22016-10-24 09:04:12 -0700538 protected void onDestroy() {
539 mDragLayout.removeDragCallback(mDragCallback);
540 super.onDestroy();
541 }
542
543 @Override
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700544 public void onActionModeStarted(ActionMode mode) {
545 super.onActionModeStarted(mode);
Christine Franks7452d3a2016-10-27 13:41:18 -0700546 if (mode.getTag() == CalculatorFormula.TAG_ACTION_MODE) {
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700547 mFormulaContainer.scrollTo(mFormulaText.getRight(), 0);
548 }
549 }
550
Chenjie Yu3937b652016-06-01 23:14:26 -0700551 /**
552 * Stop any active ActionMode or ContextMenu for copy/paste actions.
553 * Return true if there was one.
554 */
555 private boolean stopActionModeOrContextMenu() {
556 if (mResultText.stopActionModeOrContextMenu()) {
Hans Boehm1176f232015-05-11 16:26:03 -0700557 return true;
558 }
Chenjie Yu3937b652016-06-01 23:14:26 -0700559 if (mFormulaText.stopActionModeOrContextMenu()) {
Hans Boehm1176f232015-05-11 16:26:03 -0700560 return true;
561 }
562 return false;
563 }
564
Justin Klaassen4b3af052014-05-27 17:53:10 -0700565 @Override
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700566 public void onUserInteraction() {
567 super.onUserInteraction();
568
569 // If there's an animation in progress, end it immediately, so the user interaction can
570 // be handled.
571 if (mCurrentAnimator != null) {
572 mCurrentAnimator.end();
573 }
574 }
575
576 @Override
Justin Klaassen3b4d13d2014-06-06 18:18:37 +0100577 public void onBackPressed() {
Chenjie Yu3937b652016-06-01 23:14:26 -0700578 if (!stopActionModeOrContextMenu()) {
Annie Chin09547532016-10-14 10:59:07 -0700579 if (mDragLayout.isOpen()) {
Annie Chin09547532016-10-14 10:59:07 -0700580 mDragLayout.setClosed();
Annie Chin06fd3cf2016-11-07 16:04:33 -0800581 popFragmentBackstack();
Annie Chin09547532016-10-14 10:59:07 -0700582 return;
583 }
Hans Boehm1176f232015-05-11 16:26:03 -0700584 if (mPadViewPager != null && mPadViewPager.getCurrentItem() != 0) {
585 // Select the previous pad.
586 mPadViewPager.setCurrentItem(mPadViewPager.getCurrentItem() - 1);
587 } else {
588 // If the user is currently looking at the first pad (or the pad is not paged),
589 // allow the system to handle the Back button.
590 super.onBackPressed();
591 }
Justin Klaassen3b4d13d2014-06-06 18:18:37 +0100592 }
593 }
594
595 @Override
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700596 public boolean onKeyUp(int keyCode, KeyEvent event) {
Justin Klaassen83959da2016-04-06 11:55:24 -0700597 // Allow the system to handle special key codes (e.g. "BACK" or "DPAD").
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700598 switch (keyCode) {
Justin Klaassen83959da2016-04-06 11:55:24 -0700599 case KeyEvent.KEYCODE_BACK:
Christine Franksf9ba2202016-10-20 17:20:19 -0700600 case KeyEvent.KEYCODE_ESCAPE:
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700601 case KeyEvent.KEYCODE_DPAD_UP:
602 case KeyEvent.KEYCODE_DPAD_DOWN:
603 case KeyEvent.KEYCODE_DPAD_LEFT:
604 case KeyEvent.KEYCODE_DPAD_RIGHT:
605 return super.onKeyUp(keyCode, event);
606 }
607
Chenjie Yu3937b652016-06-01 23:14:26 -0700608 // Stop the action mode or context menu if it's showing.
609 stopActionModeOrContextMenu();
Justin Klaassend12e0622016-04-27 16:26:47 -0700610
Hans Boehmced295e2016-11-17 17:30:13 -0800611 // Always cancel unrequested in-progress evaluation of the main expression, so that
612 // we don't have to worry about subsequent asynchronous completion.
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700613 // Requested in-progress evaluations are handled below.
Hans Boehm31ea2522016-11-23 17:47:02 -0800614 cancelUnrequested();
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700615
616 switch (keyCode) {
617 case KeyEvent.KEYCODE_NUMPAD_ENTER:
618 case KeyEvent.KEYCODE_ENTER:
619 case KeyEvent.KEYCODE_DPAD_CENTER:
620 mCurrentButton = mEqualButton;
621 onEquals();
622 return true;
623 case KeyEvent.KEYCODE_DEL:
624 mCurrentButton = mDeleteButton;
625 onDelete();
626 return true;
Annie Chin56bcbf12016-09-23 17:04:22 -0700627 case KeyEvent.KEYCODE_CLEAR:
628 mCurrentButton = mClearButton;
629 onClear();
630 return true;
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700631 default:
632 cancelIfEvaluating(false);
633 final int raw = event.getKeyCharacterMap().get(keyCode, event.getMetaState());
634 if ((raw & KeyCharacterMap.COMBINING_ACCENT) != 0) {
635 return true; // discard
636 }
637 // Try to discard non-printing characters and the like.
638 // The user will have to explicitly delete other junk that gets past us.
639 if (Character.isIdentifierIgnorable(raw) || Character.isWhitespace(raw)) {
640 return true;
641 }
642 char c = (char) raw;
643 if (c == '=') {
644 mCurrentButton = mEqualButton;
645 onEquals();
646 } else {
647 addChars(String.valueOf(c), true);
648 redisplayAfterFormulaChange();
649 }
650 return true;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700651 }
652 }
653
Justin Klaassene2711cb2015-05-28 11:13:17 -0700654 /**
655 * Invoked whenever the inverse button is toggled to update the UI.
656 *
657 * @param showInverse {@code true} if inverse functions should be shown
658 */
659 private void onInverseToggled(boolean showInverse) {
Justin Klaassen3e223ea2016-02-05 14:18:06 -0800660 mInverseToggle.setSelected(showInverse);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700661 if (showInverse) {
662 mInverseToggle.setContentDescription(getString(R.string.desc_inv_on));
Justin Klaassen721ec842015-05-28 14:30:08 -0700663 for (View invertibleButton : mInvertibleButtons) {
664 invertibleButton.setVisibility(View.GONE);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700665 }
666 for (View inverseButton : mInverseButtons) {
667 inverseButton.setVisibility(View.VISIBLE);
668 }
669 } else {
670 mInverseToggle.setContentDescription(getString(R.string.desc_inv_off));
Justin Klaassen721ec842015-05-28 14:30:08 -0700671 for (View invertibleButton : mInvertibleButtons) {
672 invertibleButton.setVisibility(View.VISIBLE);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700673 }
674 for (View inverseButton : mInverseButtons) {
675 inverseButton.setVisibility(View.GONE);
676 }
677 }
678 }
679
680 /**
Christine Frankseeff27f2016-07-29 12:05:29 -0700681 * Invoked whenever the deg/rad mode may have changed to update the UI. Note that the mode has
682 * not necessarily actually changed where this is invoked.
Justin Klaassene2711cb2015-05-28 11:13:17 -0700683 *
684 * @param degreeMode {@code true} if in degree mode
685 */
686 private void onModeChanged(boolean degreeMode) {
687 if (degreeMode) {
Justin Klaassend48b7562015-04-16 16:51:38 -0700688 mModeView.setText(R.string.mode_deg);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700689 mModeView.setContentDescription(getString(R.string.desc_mode_deg));
690
691 mModeToggle.setText(R.string.mode_rad);
692 mModeToggle.setContentDescription(getString(R.string.desc_switch_rad));
Hans Boehmbfe8c222015-04-02 16:26:07 -0700693 } else {
Justin Klaassend48b7562015-04-16 16:51:38 -0700694 mModeView.setText(R.string.mode_rad);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700695 mModeView.setContentDescription(getString(R.string.desc_mode_rad));
696
697 mModeToggle.setText(R.string.mode_deg);
698 mModeToggle.setContentDescription(getString(R.string.desc_switch_deg));
Hans Boehmbfe8c222015-04-02 16:26:07 -0700699 }
700 }
Hans Boehm84614952014-11-25 18:46:17 -0800701
Annie Chin06fd3cf2016-11-07 16:04:33 -0800702 private void popFragmentBackstack() {
703 final FragmentManager manager = getFragmentManager();
704 if (manager == null || manager.isDestroyed()) {
705 return;
706 }
707 manager.popBackStack();
Annie Chin9a211132016-11-30 12:52:06 -0800708 manager.executePendingTransactions();
Annie Chin06fd3cf2016-11-07 16:04:33 -0800709 }
Annie Chin9a211132016-11-30 12:52:06 -0800710
Hans Boehm5d79d102015-09-16 16:33:47 -0700711 /**
712 * Switch to INPUT from RESULT state in response to input of the specified button_id.
713 * View.NO_ID is treated as an incomplete function id.
714 */
715 private void switchToInput(int button_id) {
716 if (KeyMaps.isBinary(button_id) || KeyMaps.isSuffix(button_id)) {
Hans Boehm8f051c32016-10-03 16:53:58 -0700717 mEvaluator.collapse(mEvaluator.getMaxIndex() /* Most recent history entry */);
Hans Boehm5d79d102015-09-16 16:33:47 -0700718 } else {
719 announceClearedForAccessibility();
Hans Boehm8f051c32016-10-03 16:53:58 -0700720 mEvaluator.clearMain();
Hans Boehm5d79d102015-09-16 16:33:47 -0700721 }
722 setState(CalculatorState.INPUT);
723 }
724
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700725 // Add the given button id to input expression.
726 // If appropriate, clear the expression before doing so.
727 private void addKeyToExpr(int id) {
728 if (mCurrentState == CalculatorState.ERROR) {
729 setState(CalculatorState.INPUT);
730 } else if (mCurrentState == CalculatorState.RESULT) {
Hans Boehm5d79d102015-09-16 16:33:47 -0700731 switchToInput(id);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700732 }
733 if (!mEvaluator.append(id)) {
734 // TODO: Some user visible feedback?
735 }
736 }
737
Hans Boehm017de982015-06-10 17:46:03 -0700738 /**
739 * Add the given button id to input expression, assuming it was explicitly
740 * typed/touched.
741 * We perform slightly more aggressive correction than in pasted expressions.
742 */
743 private void addExplicitKeyToExpr(int id) {
744 if (mCurrentState == CalculatorState.INPUT && id == R.id.op_sub) {
Hans Boehm8f051c32016-10-03 16:53:58 -0700745 mEvaluator.getExpr(Evaluator.MAIN_INDEX).removeTrailingAdditiveOperators();
Hans Boehm017de982015-06-10 17:46:03 -0700746 }
747 addKeyToExpr(id);
748 }
749
Hans Boehmbd01e4b2016-11-23 10:12:58 -0800750 public void evaluateInstantIfNecessary() {
751 if (mCurrentState == CalculatorState.INPUT
752 && mEvaluator.getExpr(Evaluator.MAIN_INDEX).hasInterestingOps()) {
753 mEvaluator.evaluateAndNotify(Evaluator.MAIN_INDEX, this, mResultText);
754 }
755 }
756
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700757 private void redisplayAfterFormulaChange() {
758 // TODO: Could do this more incrementally.
759 redisplayFormula();
760 setState(CalculatorState.INPUT);
Hans Boehm8f051c32016-10-03 16:53:58 -0700761 mResultText.clear();
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800762 if (haveUnprocessed()) {
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800763 // Force reevaluation when text is deleted, even if expression is unchanged.
764 mEvaluator.touch();
765 } else {
Hans Boehmbd01e4b2016-11-23 10:12:58 -0800766 evaluateInstantIfNecessary();
Hans Boehmc023b732015-04-29 11:30:47 -0700767 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700768 }
769
Hans Boehm52d477a2016-04-01 17:42:50 -0700770 /**
771 * Show the toolbar.
772 * Automatically hide it again if it's not relevant to current formula.
773 */
774 private void showAndMaybeHideToolbar() {
775 final boolean shouldBeVisible =
776 mCurrentState == CalculatorState.INPUT && mEvaluator.hasTrigFuncs();
777 mDisplayView.showToolbar(!shouldBeVisible);
778 }
779
780 /**
781 * Display or hide the toolbar depending on calculator state.
782 */
783 private void showOrHideToolbar() {
784 final boolean shouldBeVisible =
785 mCurrentState == CalculatorState.INPUT && mEvaluator.hasTrigFuncs();
786 if (shouldBeVisible) {
787 mDisplayView.showToolbar(false);
788 } else {
789 mDisplayView.hideToolbar();
790 }
791 }
792
Justin Klaassen4b3af052014-05-27 17:53:10 -0700793 public void onButtonClick(View view) {
Hans Boehmc1ea0912015-06-19 15:05:07 -0700794 // Any animation is ended before we get here.
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700795 mCurrentButton = view;
Chenjie Yu3937b652016-06-01 23:14:26 -0700796 stopActionModeOrContextMenu();
Justin Klaassen9d33cdc2016-02-21 14:16:14 -0800797
Hans Boehmc1ea0912015-06-19 15:05:07 -0700798 // See onKey above for the rationale behind some of the behavior below:
Hans Boehm31ea2522016-11-23 17:47:02 -0800799 cancelUnrequested();
Justin Klaassen9d33cdc2016-02-21 14:16:14 -0800800
Justin Klaassend48b7562015-04-16 16:51:38 -0700801 final int id = view.getId();
Hans Boehm84614952014-11-25 18:46:17 -0800802 switch (id) {
Justin Klaassen4b3af052014-05-27 17:53:10 -0700803 case R.id.eq:
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700804 onEquals();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700805 break;
806 case R.id.del:
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700807 onDelete();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700808 break;
809 case R.id.clr:
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700810 onClear();
Hans Boehm52d477a2016-04-01 17:42:50 -0700811 return; // Toolbar visibility adjusted at end of animation.
Justin Klaassene2711cb2015-05-28 11:13:17 -0700812 case R.id.toggle_inv:
813 final boolean selected = !mInverseToggle.isSelected();
814 mInverseToggle.setSelected(selected);
815 onInverseToggled(selected);
Hans Boehmc1ea0912015-06-19 15:05:07 -0700816 if (mCurrentState == CalculatorState.RESULT) {
817 mResultText.redisplay(); // In case we cancelled reevaluation.
818 }
Justin Klaassene2711cb2015-05-28 11:13:17 -0700819 break;
820 case R.id.toggle_mode:
Hans Boehmc1ea0912015-06-19 15:05:07 -0700821 cancelIfEvaluating(false);
Hans Boehm8f051c32016-10-03 16:53:58 -0700822 final boolean mode = !mEvaluator.getDegreeMode(Evaluator.MAIN_INDEX);
823 if (mCurrentState == CalculatorState.RESULT
824 && mEvaluator.getExpr(Evaluator.MAIN_INDEX).hasTrigFuncs()) {
825 // Capture current result evaluated in old mode.
826 mEvaluator.collapse(mEvaluator.getMaxIndex());
Hans Boehmbfe8c222015-04-02 16:26:07 -0700827 redisplayFormula();
828 }
829 // In input mode, we reinterpret already entered trig functions.
830 mEvaluator.setDegreeMode(mode);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700831 onModeChanged(mode);
Christine Frankseeff27f2016-07-29 12:05:29 -0700832 // Show the toolbar to highlight the mode change.
833 showAndMaybeHideToolbar();
Hans Boehmbfe8c222015-04-02 16:26:07 -0700834 setState(CalculatorState.INPUT);
Justin Klaassen44595162015-05-28 17:55:20 -0700835 mResultText.clear();
Hans Boehmbd01e4b2016-11-23 10:12:58 -0800836 if (!haveUnprocessed()) {
837 evaluateInstantIfNecessary();
Hans Boehmc023b732015-04-29 11:30:47 -0700838 }
Christine Frankseeff27f2016-07-29 12:05:29 -0700839 return;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700840 default:
Hans Boehmc1ea0912015-06-19 15:05:07 -0700841 cancelIfEvaluating(false);
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800842 if (haveUnprocessed()) {
843 // For consistency, append as uninterpreted characters.
844 // This may actually be useful for a left parenthesis.
845 addChars(KeyMaps.toString(this, id), true);
846 } else {
847 addExplicitKeyToExpr(id);
848 redisplayAfterFormulaChange();
849 }
Justin Klaassen4b3af052014-05-27 17:53:10 -0700850 break;
851 }
Hans Boehm52d477a2016-04-01 17:42:50 -0700852 showOrHideToolbar();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700853 }
854
Hans Boehm84614952014-11-25 18:46:17 -0800855 void redisplayFormula() {
Hans Boehm8f051c32016-10-03 16:53:58 -0700856 SpannableStringBuilder formula
857 = mEvaluator.getExpr(Evaluator.MAIN_INDEX).toSpannableStringBuilder(this);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700858 if (mUnprocessedChars != null) {
859 // Add and highlight characters we couldn't process.
Hans Boehm8a4f81c2015-07-09 10:41:25 -0700860 formula.append(mUnprocessedChars, mUnprocessedColorSpan,
861 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700862 }
Hans Boehm8a4f81c2015-07-09 10:41:25 -0700863 mFormulaText.changeTextTo(formula);
Annie Chinf360ef02016-03-10 13:45:39 -0800864 mFormulaText.setContentDescription(TextUtils.isEmpty(formula)
Justin Klaassend1831412016-07-19 21:59:10 -0700865 ? getString(R.string.desc_formula) : null);
Hans Boehm84614952014-11-25 18:46:17 -0800866 }
867
Justin Klaassen4b3af052014-05-27 17:53:10 -0700868 @Override
869 public boolean onLongClick(View view) {
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700870 mCurrentButton = view;
871
Justin Klaassen4b3af052014-05-27 17:53:10 -0700872 if (view.getId() == R.id.del) {
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700873 onClear();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700874 return true;
875 }
876 return false;
877 }
878
Hans Boehm84614952014-11-25 18:46:17 -0800879 // Initial evaluation completed successfully. Initiate display.
Hans Boehm8f051c32016-10-03 16:53:58 -0700880 public void onEvaluate(long index, int initDisplayPrec, int msd, int leastDigPos,
Hans Boehma0e45f32015-05-30 13:20:35 -0700881 String truncatedWholeNumber) {
Hans Boehm8f051c32016-10-03 16:53:58 -0700882 if (index != Evaluator.MAIN_INDEX) {
883 throw new AssertionError("Unexpected evaluation result index\n");
884 }
Annie Chin37c33b62016-11-22 14:46:28 -0800885
Justin Klaassend48b7562015-04-16 16:51:38 -0700886 // Invalidate any options that may depend on the current result.
887 invalidateOptionsMenu();
888
Hans Boehm8f051c32016-10-03 16:53:58 -0700889 mResultText.onEvaluate(index, initDisplayPrec, msd, leastDigPos, truncatedWholeNumber);
Hans Boehm31ea2522016-11-23 17:47:02 -0800890 if (mCurrentState != CalculatorState.INPUT) {
891 // In EVALUATE, INIT, or INIT_FOR_RESULT state.
892 onResult(mCurrentState == CalculatorState.EVALUATE);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700893 }
Hans Boehm84614952014-11-25 18:46:17 -0800894 }
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700895
Hans Boehmc1ea0912015-06-19 15:05:07 -0700896 // Reset state to reflect evaluator cancellation. Invoked by evaluator.
Hans Boehm8f051c32016-10-03 16:53:58 -0700897 public void onCancelled(long index) {
898 // Index is Evaluator.MAIN_INDEX. We should be in EVALUATE state.
Hans Boehm84614952014-11-25 18:46:17 -0800899 setState(CalculatorState.INPUT);
Hans Boehm8f051c32016-10-03 16:53:58 -0700900 mResultText.onCancelled(index);
Hans Boehm84614952014-11-25 18:46:17 -0800901 }
902
903 // Reevaluation completed; ask result to redisplay current value.
Hans Boehm8f051c32016-10-03 16:53:58 -0700904 public void onReevaluate(long index)
Hans Boehm84614952014-11-25 18:46:17 -0800905 {
Hans Boehm8f051c32016-10-03 16:53:58 -0700906 // Index is Evaluator.MAIN_INDEX.
907 mResultText.onReevaluate(index);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700908 }
909
Justin Klaassenfed941a2014-06-09 18:42:40 +0100910 @Override
911 public void onTextSizeChanged(final TextView textView, float oldSize) {
912 if (mCurrentState != CalculatorState.INPUT) {
913 // Only animate text changes that occur from user input.
914 return;
915 }
916
917 // Calculate the values needed to perform the scale and translation animations,
918 // maintaining the same apparent baseline for the displayed text.
919 final float textScale = oldSize / textView.getTextSize();
920 final float translationX = (1.0f - textScale) *
921 (textView.getWidth() / 2.0f - textView.getPaddingEnd());
922 final float translationY = (1.0f - textScale) *
923 (textView.getHeight() / 2.0f - textView.getPaddingBottom());
924
925 final AnimatorSet animatorSet = new AnimatorSet();
926 animatorSet.playTogether(
927 ObjectAnimator.ofFloat(textView, View.SCALE_X, textScale, 1.0f),
928 ObjectAnimator.ofFloat(textView, View.SCALE_Y, textScale, 1.0f),
929 ObjectAnimator.ofFloat(textView, View.TRANSLATION_X, translationX, 0.0f),
930 ObjectAnimator.ofFloat(textView, View.TRANSLATION_Y, translationY, 0.0f));
Justin Klaassen94db7202014-06-11 11:22:31 -0700931 animatorSet.setDuration(getResources().getInteger(android.R.integer.config_mediumAnimTime));
Justin Klaassenfed941a2014-06-09 18:42:40 +0100932 animatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
933 animatorSet.start();
934 }
935
Hans Boehmc1ea0912015-06-19 15:05:07 -0700936 /**
937 * Cancel any in-progress explicitly requested evaluations.
938 * @param quiet suppress pop-up message. Explicit evaluation can change the expression
939 value, and certainly changes the display, so it seems reasonable to warn.
940 * @return true if there was such an evaluation
941 */
942 private boolean cancelIfEvaluating(boolean quiet) {
943 if (mCurrentState == CalculatorState.EVALUATE) {
Hans Boehm31ea2522016-11-23 17:47:02 -0800944 mEvaluator.cancel(Evaluator.MAIN_INDEX, quiet);
Hans Boehmc1ea0912015-06-19 15:05:07 -0700945 return true;
946 } else {
947 return false;
948 }
949 }
950
Hans Boehm31ea2522016-11-23 17:47:02 -0800951
952 private void cancelUnrequested() {
953 if (mCurrentState == CalculatorState.INPUT) {
954 mEvaluator.cancel(Evaluator.MAIN_INDEX, true);
955 }
956 }
957
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800958 private boolean haveUnprocessed() {
959 return mUnprocessedChars != null && !mUnprocessedChars.isEmpty();
960 }
961
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700962 private void onEquals() {
Hans Boehm56d6e762016-06-06 11:46:29 -0700963 // Ignore if in non-INPUT state, or if there are no operators.
Justin Klaassena8075af2016-07-27 15:24:45 -0700964 if (mCurrentState == CalculatorState.INPUT) {
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800965 if (haveUnprocessed()) {
Justin Klaassena8075af2016-07-27 15:24:45 -0700966 setState(CalculatorState.EVALUATE);
Hans Boehm8f051c32016-10-03 16:53:58 -0700967 onError(Evaluator.MAIN_INDEX, R.string.error_syntax);
968 } else if (mEvaluator.getExpr(Evaluator.MAIN_INDEX).hasInterestingOps()) {
Justin Klaassena8075af2016-07-27 15:24:45 -0700969 setState(CalculatorState.EVALUATE);
Hans Boehm8f051c32016-10-03 16:53:58 -0700970 mEvaluator.requireResult(Evaluator.MAIN_INDEX, this, mResultText);
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800971 }
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700972 }
973 }
974
975 private void onDelete() {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700976 // Delete works like backspace; remove the last character or operator from the expression.
977 // Note that we handle keyboard delete exactly like the delete button. For
978 // example the delete button can be used to delete a character from an incomplete
979 // function name typed on a physical keyboard.
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700980 // This should be impossible in RESULT state.
Hans Boehmc1ea0912015-06-19 15:05:07 -0700981 // If there is an in-progress explicit evaluation, just cancel it and return.
982 if (cancelIfEvaluating(false)) return;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700983 setState(CalculatorState.INPUT);
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800984 if (haveUnprocessed()) {
985 mUnprocessedChars = mUnprocessedChars.substring(0, mUnprocessedChars.length() - 1);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700986 } else {
Hans Boehmc023b732015-04-29 11:30:47 -0700987 mEvaluator.delete();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700988 }
Hans Boehm8f051c32016-10-03 16:53:58 -0700989 if (mEvaluator.getExpr(Evaluator.MAIN_INDEX).isEmpty() && !haveUnprocessed()) {
Hans Boehmdb6f9992015-08-19 12:32:56 -0700990 // Resulting formula won't be announced, since it's empty.
991 announceClearedForAccessibility();
992 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700993 redisplayAfterFormulaChange();
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700994 }
995
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700996 private void reveal(View sourceView, int colorRes, AnimatorListener listener) {
Justin Klaassen06360f92014-08-28 11:08:44 -0700997 final ViewGroupOverlay groupOverlay =
998 (ViewGroupOverlay) getWindow().getDecorView().getOverlay();
Justin Klaassen8fff1442014-06-19 10:43:29 -0700999
1000 final Rect displayRect = new Rect();
Justin Klaassen06360f92014-08-28 11:08:44 -07001001 mDisplayView.getGlobalVisibleRect(displayRect);
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001002
1003 // Make reveal cover the display and status bar.
1004 final View revealView = new View(this);
Justin Klaassen8fff1442014-06-19 10:43:29 -07001005 revealView.setBottom(displayRect.bottom);
1006 revealView.setLeft(displayRect.left);
1007 revealView.setRight(displayRect.right);
Chenjie Yu3937b652016-06-01 23:14:26 -07001008 revealView.setBackgroundColor(ContextCompat.getColor(this, colorRes));
Justin Klaassen06360f92014-08-28 11:08:44 -07001009 groupOverlay.add(revealView);
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001010
Justin Klaassen4b3af052014-05-27 17:53:10 -07001011 final int[] clearLocation = new int[2];
1012 sourceView.getLocationInWindow(clearLocation);
1013 clearLocation[0] += sourceView.getWidth() / 2;
1014 clearLocation[1] += sourceView.getHeight() / 2;
1015
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001016 final int revealCenterX = clearLocation[0] - revealView.getLeft();
1017 final int revealCenterY = clearLocation[1] - revealView.getTop();
Justin Klaassen4b3af052014-05-27 17:53:10 -07001018
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001019 final double x1_2 = Math.pow(revealView.getLeft() - revealCenterX, 2);
1020 final double x2_2 = Math.pow(revealView.getRight() - revealCenterX, 2);
1021 final double y_2 = Math.pow(revealView.getTop() - revealCenterY, 2);
Justin Klaassen4b3af052014-05-27 17:53:10 -07001022 final float revealRadius = (float) Math.max(Math.sqrt(x1_2 + y_2), Math.sqrt(x2_2 + y_2));
1023
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001024 final Animator revealAnimator =
1025 ViewAnimationUtils.createCircularReveal(revealView,
ztenghui3d6ecaf2014-06-05 09:56:00 -07001026 revealCenterX, revealCenterY, 0.0f, revealRadius);
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001027 revealAnimator.setDuration(
Justin Klaassen4b3af052014-05-27 17:53:10 -07001028 getResources().getInteger(android.R.integer.config_longAnimTime));
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001029 revealAnimator.addListener(listener);
Justin Klaassen4b3af052014-05-27 17:53:10 -07001030
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001031 final Animator alphaAnimator = ObjectAnimator.ofFloat(revealView, View.ALPHA, 0.0f);
Justin Klaassen4b3af052014-05-27 17:53:10 -07001032 alphaAnimator.setDuration(
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001033 getResources().getInteger(android.R.integer.config_mediumAnimTime));
Justin Klaassen4b3af052014-05-27 17:53:10 -07001034
1035 final AnimatorSet animatorSet = new AnimatorSet();
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001036 animatorSet.play(revealAnimator).before(alphaAnimator);
Justin Klaassen4b3af052014-05-27 17:53:10 -07001037 animatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
1038 animatorSet.addListener(new AnimatorListenerAdapter() {
1039 @Override
Justin Klaassen4b3af052014-05-27 17:53:10 -07001040 public void onAnimationEnd(Animator animator) {
Justin Klaassen8fff1442014-06-19 10:43:29 -07001041 groupOverlay.remove(revealView);
Justin Klaassen4b3af052014-05-27 17:53:10 -07001042 mCurrentAnimator = null;
1043 }
1044 });
1045
1046 mCurrentAnimator = animatorSet;
1047 animatorSet.start();
1048 }
1049
Hans Boehmdb6f9992015-08-19 12:32:56 -07001050 private void announceClearedForAccessibility() {
1051 mResultText.announceForAccessibility(getResources().getString(R.string.cleared));
Hans Boehmccc55662015-07-07 14:16:59 -07001052 }
1053
Hans Boehm9db3ee22016-11-18 10:09:47 -08001054 public void onClearAnimationEnd() {
1055 mUnprocessedChars = null;
1056 mResultText.clear();
1057 mEvaluator.clearMain();
1058 setState(CalculatorState.INPUT);
1059 redisplayFormula();
1060 }
1061
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001062 private void onClear() {
Hans Boehm8f051c32016-10-03 16:53:58 -07001063 if (mEvaluator.getExpr(Evaluator.MAIN_INDEX).isEmpty() && !haveUnprocessed()) {
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001064 return;
1065 }
Hans Boehmc1ea0912015-06-19 15:05:07 -07001066 cancelIfEvaluating(true);
Hans Boehmdb6f9992015-08-19 12:32:56 -07001067 announceClearedForAccessibility();
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001068 reveal(mCurrentButton, R.color.calculator_accent_color, new AnimatorListenerAdapter() {
1069 @Override
1070 public void onAnimationEnd(Animator animation) {
Hans Boehm9db3ee22016-11-18 10:09:47 -08001071 onClearAnimationEnd();
Hans Boehm52d477a2016-04-01 17:42:50 -07001072 showOrHideToolbar();
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001073 }
1074 });
1075 }
1076
Hans Boehm84614952014-11-25 18:46:17 -08001077 // Evaluation encountered en error. Display the error.
Hans Boehm8f051c32016-10-03 16:53:58 -07001078 @Override
1079 public void onError(final long index, final int errorResourceId) {
1080 if (index != Evaluator.MAIN_INDEX) {
1081 throw new AssertionError("Unexpected error source");
1082 }
Hans Boehmfbcef702015-04-27 18:07:47 -07001083 if (mCurrentState == CalculatorState.EVALUATE) {
1084 setState(CalculatorState.ANIMATE);
Hans Boehmccc55662015-07-07 14:16:59 -07001085 mResultText.announceForAccessibility(getResources().getString(errorResourceId));
Hans Boehmfbcef702015-04-27 18:07:47 -07001086 reveal(mCurrentButton, R.color.calculator_error_color,
1087 new AnimatorListenerAdapter() {
1088 @Override
1089 public void onAnimationEnd(Animator animation) {
1090 setState(CalculatorState.ERROR);
Hans Boehm8f051c32016-10-03 16:53:58 -07001091 mResultText.onError(index, errorResourceId);
Hans Boehmfbcef702015-04-27 18:07:47 -07001092 }
1093 });
Hans Boehm31ea2522016-11-23 17:47:02 -08001094 } else if (mCurrentState == CalculatorState.INIT
1095 || mCurrentState == CalculatorState.INIT_FOR_RESULT /* very unlikely */) {
Hans Boehmfbcef702015-04-27 18:07:47 -07001096 setState(CalculatorState.ERROR);
Hans Boehm8f051c32016-10-03 16:53:58 -07001097 mResultText.onError(index, errorResourceId);
Hans Boehmc023b732015-04-29 11:30:47 -07001098 } else {
Justin Klaassen44595162015-05-28 17:55:20 -07001099 mResultText.clear();
Justin Klaassen2be4fdb2014-08-06 19:54:09 -07001100 }
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001101 }
1102
Hans Boehm84614952014-11-25 18:46:17 -08001103 // Animate movement of result into the top formula slot.
1104 // Result window now remains translated in the top slot while the result is displayed.
1105 // (We convert it back to formula use only when the user provides new input.)
Justin Klaassen44595162015-05-28 17:55:20 -07001106 // Historical note: In the Lollipop version, this invisibly and instantaneously moved
Hans Boehm84614952014-11-25 18:46:17 -08001107 // formula and result displays back at the end of the animation. We no longer do that,
1108 // so that we can continue to properly support scrolling of the result.
1109 // We assume the result already contains the text to be expanded.
1110 private void onResult(boolean animate) {
Justin Klaassen44595162015-05-28 17:55:20 -07001111 // Calculate the textSize that would be used to display the result in the formula.
1112 // For scrollable results just use the minimum textSize to maximize the number of digits
1113 // that are visible on screen.
1114 float textSize = mFormulaText.getMinimumTextSize();
1115 if (!mResultText.isScrollable()) {
1116 textSize = mFormulaText.getVariableTextSize(mResultText.getText().toString());
1117 }
1118
1119 // Scale the result to match the calculated textSize, minimizing the jump-cut transition
1120 // when a result is reused in a subsequent expression.
1121 final float resultScale = textSize / mResultText.getTextSize();
1122
1123 // Set the result's pivot to match its gravity.
1124 mResultText.setPivotX(mResultText.getWidth() - mResultText.getPaddingRight());
1125 mResultText.setPivotY(mResultText.getHeight() - mResultText.getPaddingBottom());
1126
1127 // Calculate the necessary translations so the result takes the place of the formula and
1128 // the formula moves off the top of the screen.
Annie Chin28589dc2016-06-09 17:50:51 -07001129 final float resultTranslationY = (mFormulaContainer.getBottom() - mResultText.getBottom())
1130 - (mFormulaText.getPaddingBottom() - mResultText.getPaddingBottom());
1131 float formulaTranslationY = -mFormulaContainer.getBottom();
Annie Chin26e159e2016-05-18 15:17:14 -07001132 if (mOneLine) {
1133 // Position the result text.
1134 mResultText.setY(mResultText.getBottom());
Annie Chin28589dc2016-06-09 17:50:51 -07001135 formulaTranslationY = -(findViewById(R.id.toolbar).getBottom()
1136 + mFormulaContainer.getBottom());
Annie Chin26e159e2016-05-18 15:17:14 -07001137 }
Justin Klaassen4b3af052014-05-27 17:53:10 -07001138
Justin Klaassen44595162015-05-28 17:55:20 -07001139 // Change the result's textColor to match the formula.
1140 final int formulaTextColor = mFormulaText.getCurrentTextColor();
1141
Hans Boehm84614952014-11-25 18:46:17 -08001142 if (animate) {
Hans Boehmccc55662015-07-07 14:16:59 -07001143 mResultText.announceForAccessibility(getResources().getString(R.string.desc_eq));
1144 mResultText.announceForAccessibility(mResultText.getText());
Hans Boehmc1ea0912015-06-19 15:05:07 -07001145 setState(CalculatorState.ANIMATE);
Hans Boehm84614952014-11-25 18:46:17 -08001146 final AnimatorSet animatorSet = new AnimatorSet();
1147 animatorSet.playTogether(
Justin Klaassen44595162015-05-28 17:55:20 -07001148 ObjectAnimator.ofPropertyValuesHolder(mResultText,
1149 PropertyValuesHolder.ofFloat(View.SCALE_X, resultScale),
1150 PropertyValuesHolder.ofFloat(View.SCALE_Y, resultScale),
1151 PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, resultTranslationY)),
1152 ObjectAnimator.ofArgb(mResultText, TEXT_COLOR, formulaTextColor),
Annie Chine918fd22016-03-09 11:07:54 -08001153 ObjectAnimator.ofFloat(mFormulaContainer, View.TRANSLATION_Y,
1154 formulaTranslationY));
Justin Klaassen44595162015-05-28 17:55:20 -07001155 animatorSet.setDuration(getResources().getInteger(
1156 android.R.integer.config_longAnimTime));
Hans Boehm84614952014-11-25 18:46:17 -08001157 animatorSet.addListener(new AnimatorListenerAdapter() {
1158 @Override
Hans Boehm84614952014-11-25 18:46:17 -08001159 public void onAnimationEnd(Animator animation) {
Hans Boehm8f051c32016-10-03 16:53:58 -07001160 // Add current result to history.
1161 mEvaluator.preserve(true);
Hans Boehm84614952014-11-25 18:46:17 -08001162 setState(CalculatorState.RESULT);
1163 mCurrentAnimator = null;
1164 }
1165 });
Justin Klaassen4b3af052014-05-27 17:53:10 -07001166
Hans Boehm84614952014-11-25 18:46:17 -08001167 mCurrentAnimator = animatorSet;
1168 animatorSet.start();
Hans Boehm8f051c32016-10-03 16:53:58 -07001169 } else /* No animation desired; get there fast when restarting */ {
Justin Klaassen44595162015-05-28 17:55:20 -07001170 mResultText.setScaleX(resultScale);
1171 mResultText.setScaleY(resultScale);
1172 mResultText.setTranslationY(resultTranslationY);
1173 mResultText.setTextColor(formulaTextColor);
Annie Chine918fd22016-03-09 11:07:54 -08001174 mFormulaContainer.setTranslationY(formulaTranslationY);
Hans Boehm8f051c32016-10-03 16:53:58 -07001175 mEvaluator.represerve();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001176 setState(CalculatorState.RESULT);
Hans Boehm84614952014-11-25 18:46:17 -08001177 }
Justin Klaassen4b3af052014-05-27 17:53:10 -07001178 }
Hans Boehm84614952014-11-25 18:46:17 -08001179
1180 // Restore positions of the formula and result displays back to their original,
1181 // pre-animation state.
1182 private void restoreDisplayPositions() {
1183 // Clear result.
Justin Klaassen44595162015-05-28 17:55:20 -07001184 mResultText.setText("");
Hans Boehm84614952014-11-25 18:46:17 -08001185 // Reset all of the values modified during the animation.
Justin Klaassen44595162015-05-28 17:55:20 -07001186 mResultText.setScaleX(1.0f);
1187 mResultText.setScaleY(1.0f);
1188 mResultText.setTranslationX(0.0f);
1189 mResultText.setTranslationY(0.0f);
Annie Chine918fd22016-03-09 11:07:54 -08001190 mFormulaContainer.setTranslationY(0.0f);
Hans Boehm84614952014-11-25 18:46:17 -08001191
Hans Boehm08e8f322015-04-21 13:18:38 -07001192 mFormulaText.requestFocus();
Hans Boehm5e6a0ca2015-09-22 17:09:01 -07001193 }
1194
1195 @Override
1196 public void onClick(AlertDialogFragment fragment, int which) {
1197 if (which == DialogInterface.BUTTON_POSITIVE) {
1198 // Timeout extension request.
Hans Boehm8f051c32016-10-03 16:53:58 -07001199 mEvaluator.setLongTimeout();
Hans Boehm5e6a0ca2015-09-22 17:09:01 -07001200 }
1201 }
Hans Boehm84614952014-11-25 18:46:17 -08001202
Justin Klaassend48b7562015-04-16 16:51:38 -07001203 @Override
1204 public boolean onCreateOptionsMenu(Menu menu) {
Justin Klaassend36d63e2015-05-05 12:59:36 -07001205 super.onCreateOptionsMenu(menu);
1206
1207 getMenuInflater().inflate(R.menu.activity_calculator, menu);
Justin Klaassend48b7562015-04-16 16:51:38 -07001208 return true;
1209 }
1210
1211 @Override
1212 public boolean onPrepareOptionsMenu(Menu menu) {
Justin Klaassend36d63e2015-05-05 12:59:36 -07001213 super.onPrepareOptionsMenu(menu);
1214
1215 // Show the leading option when displaying a result.
1216 menu.findItem(R.id.menu_leading).setVisible(mCurrentState == CalculatorState.RESULT);
1217
1218 // Show the fraction option when displaying a rational result.
1219 menu.findItem(R.id.menu_fraction).setVisible(mCurrentState == CalculatorState.RESULT
Hans Boehm8f051c32016-10-03 16:53:58 -07001220 && mEvaluator.getResult(Evaluator.MAIN_INDEX).exactlyDisplayable());
Justin Klaassend36d63e2015-05-05 12:59:36 -07001221
Justin Klaassend48b7562015-04-16 16:51:38 -07001222 return true;
Hans Boehm84614952014-11-25 18:46:17 -08001223 }
1224
1225 @Override
Justin Klaassend48b7562015-04-16 16:51:38 -07001226 public boolean onOptionsItemSelected(MenuItem item) {
Hans Boehm84614952014-11-25 18:46:17 -08001227 switch (item.getItemId()) {
Annie Chinabd202f2016-10-14 14:23:45 -07001228 case R.id.menu_history:
Annie Chin09547532016-10-14 10:59:07 -07001229 showHistoryFragment(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
1230 mDragLayout.setOpen();
Annie Chinabd202f2016-10-14 14:23:45 -07001231 return true;
Justin Klaassend36d63e2015-05-05 12:59:36 -07001232 case R.id.menu_leading:
1233 displayFull();
Hans Boehm84614952014-11-25 18:46:17 -08001234 return true;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001235 case R.id.menu_fraction:
1236 displayFraction();
1237 return true;
Justin Klaassend36d63e2015-05-05 12:59:36 -07001238 case R.id.menu_licenses:
1239 startActivity(new Intent(this, Licenses.class));
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001240 return true;
Hans Boehm84614952014-11-25 18:46:17 -08001241 default:
1242 return super.onOptionsItemSelected(item);
1243 }
1244 }
1245
Hans Boehm31ea2522016-11-23 17:47:02 -08001246 /**
1247 * Change evaluation state to one that's friendly to the history fragment.
1248 * Return false if that was not easily possible.
1249 */
1250 private boolean prepareForHistory() {
1251 if (mCurrentState == CalculatorState.ANIMATE) {
1252 throw new AssertionError("onUserInteraction should have ended animation");
1253 } else if (mCurrentState == CalculatorState.EVALUATE
1254 || mCurrentState == CalculatorState.INIT) {
1255 // Easiest to just refuse. Otherwise we can see a state change
1256 // while in history mode, which causes all sorts of problems.
1257 // TODO: Consider other alternatives. If we're just doing the decimal conversion
1258 // at the end of an evaluation, we could treat this as RESULT state.
1259 return false;
1260 }
1261 // We should be in INPUT, INIT_FOR_RESULT, RESULT, or ERROR state.
1262 return true;
1263 }
1264
Annie Chin09547532016-10-14 10:59:07 -07001265 private void showHistoryFragment(int transit) {
Annie Chin06fd3cf2016-11-07 16:04:33 -08001266 final FragmentManager manager = getFragmentManager();
1267 if (manager == null || manager.isDestroyed()) {
1268 return;
1269 }
Hans Boehm31ea2522016-11-23 17:47:02 -08001270 if (!prepareForHistory()) {
1271 return;
1272 }
Annie Chind0f87d22016-10-24 09:04:12 -07001273 if (!mDragLayout.isOpen()) {
Annie Chin450de8a2016-11-23 10:03:56 -08001274 manager.beginTransaction()
Annie Chind0f87d22016-10-24 09:04:12 -07001275 .replace(R.id.history_frame, mHistoryFragment, HistoryFragment.TAG)
1276 .setTransition(transit)
1277 .addToBackStack(HistoryFragment.TAG)
1278 .commit();
Annie Chin450de8a2016-11-23 10:03:56 -08001279 manager.executePendingTransactions();
Annie Chind0f87d22016-10-24 09:04:12 -07001280 }
Annie Chin06fd3cf2016-11-07 16:04:33 -08001281 // TODO: pass current scroll position of result
Annie Chin09547532016-10-14 10:59:07 -07001282 }
1283
Christine Franks7452d3a2016-10-27 13:41:18 -07001284 private void displayMessage(String title, String message) {
1285 AlertDialogFragment.showMessageDialog(this, title, message, null);
Hans Boehm84614952014-11-25 18:46:17 -08001286 }
1287
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001288 private void displayFraction() {
Hans Boehm8f051c32016-10-03 16:53:58 -07001289 UnifiedReal result = mEvaluator.getResult(Evaluator.MAIN_INDEX);
Christine Franks7452d3a2016-10-27 13:41:18 -07001290 displayMessage(getString(R.string.menu_fraction),
1291 KeyMaps.translateResult(result.toNiceString()));
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001292 }
1293
1294 // Display full result to currently evaluated precision
1295 private void displayFull() {
1296 Resources res = getResources();
Hans Boehm24c91ed2016-06-30 18:53:44 -07001297 String msg = mResultText.getFullText(true /* withSeparators */) + " ";
Justin Klaassen44595162015-05-28 17:55:20 -07001298 if (mResultText.fullTextIsExact()) {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001299 msg += res.getString(R.string.exact);
1300 } else {
1301 msg += res.getString(R.string.approximate);
1302 }
Christine Franks7452d3a2016-10-27 13:41:18 -07001303 displayMessage(getString(R.string.menu_leading), msg);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001304 }
1305
Hans Boehm017de982015-06-10 17:46:03 -07001306 /**
1307 * Add input characters to the end of the expression.
1308 * Map them to the appropriate button pushes when possible. Leftover characters
1309 * are added to mUnprocessedChars, which is presumed to immediately precede the newly
1310 * added characters.
Hans Boehm65a99a42016-02-03 18:16:07 -08001311 * @param moreChars characters to be added
1312 * @param explicit these characters were explicitly typed by the user, not pasted
Hans Boehm017de982015-06-10 17:46:03 -07001313 */
1314 private void addChars(String moreChars, boolean explicit) {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001315 if (mUnprocessedChars != null) {
1316 moreChars = mUnprocessedChars + moreChars;
1317 }
1318 int current = 0;
1319 int len = moreChars.length();
Hans Boehm0b9806f2015-06-29 16:07:15 -07001320 boolean lastWasDigit = false;
Hans Boehm5d79d102015-09-16 16:33:47 -07001321 if (mCurrentState == CalculatorState.RESULT && len != 0) {
1322 // Clear display immediately for incomplete function name.
1323 switchToInput(KeyMaps.keyForChar(moreChars.charAt(current)));
1324 }
Hans Boehm24c91ed2016-06-30 18:53:44 -07001325 char groupingSeparator = KeyMaps.translateResult(",").charAt(0);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001326 while (current < len) {
1327 char c = moreChars.charAt(current);
Hans Boehm24c91ed2016-06-30 18:53:44 -07001328 if (Character.isSpaceChar(c) || c == groupingSeparator) {
1329 ++current;
1330 continue;
1331 }
Hans Boehm013969e2015-04-13 20:29:47 -07001332 int k = KeyMaps.keyForChar(c);
Hans Boehm0b9806f2015-06-29 16:07:15 -07001333 if (!explicit) {
1334 int expEnd;
1335 if (lastWasDigit && current !=
1336 (expEnd = Evaluator.exponentEnd(moreChars, current))) {
1337 // Process scientific notation with 'E' when pasting, in spite of ambiguity
1338 // with base of natural log.
1339 // Otherwise the 10^x key is the user's friend.
1340 mEvaluator.addExponent(moreChars, current, expEnd);
1341 current = expEnd;
1342 lastWasDigit = false;
1343 continue;
1344 } else {
1345 boolean isDigit = KeyMaps.digVal(k) != KeyMaps.NOT_DIGIT;
1346 if (current == 0 && (isDigit || k == R.id.dec_point)
Hans Boehm8f051c32016-10-03 16:53:58 -07001347 && mEvaluator.getExpr(Evaluator.MAIN_INDEX).hasTrailingConstant()) {
Hans Boehm0b9806f2015-06-29 16:07:15 -07001348 // Refuse to concatenate pasted content to trailing constant.
1349 // This makes pasting of calculator results more consistent, whether or
1350 // not the old calculator instance is still around.
1351 addKeyToExpr(R.id.op_mul);
1352 }
1353 lastWasDigit = (isDigit || lastWasDigit && k == R.id.dec_point);
1354 }
1355 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001356 if (k != View.NO_ID) {
1357 mCurrentButton = findViewById(k);
Hans Boehm017de982015-06-10 17:46:03 -07001358 if (explicit) {
1359 addExplicitKeyToExpr(k);
1360 } else {
1361 addKeyToExpr(k);
1362 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001363 if (Character.isSurrogate(c)) {
1364 current += 2;
1365 } else {
1366 ++current;
1367 }
1368 continue;
1369 }
Hans Boehm013969e2015-04-13 20:29:47 -07001370 int f = KeyMaps.funForString(moreChars, current);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001371 if (f != View.NO_ID) {
1372 mCurrentButton = findViewById(f);
Hans Boehm017de982015-06-10 17:46:03 -07001373 if (explicit) {
1374 addExplicitKeyToExpr(f);
1375 } else {
1376 addKeyToExpr(f);
1377 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001378 if (f == R.id.op_sqrt) {
1379 // Square root entered as function; don't lose the parenthesis.
1380 addKeyToExpr(R.id.lparen);
1381 }
1382 current = moreChars.indexOf('(', current) + 1;
1383 continue;
1384 }
1385 // There are characters left, but we can't convert them to button presses.
1386 mUnprocessedChars = moreChars.substring(current);
1387 redisplayAfterFormulaChange();
Hans Boehm52d477a2016-04-01 17:42:50 -07001388 showOrHideToolbar();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001389 return;
1390 }
1391 mUnprocessedChars = null;
1392 redisplayAfterFormulaChange();
Hans Boehm52d477a2016-04-01 17:42:50 -07001393 showOrHideToolbar();
Hans Boehm84614952014-11-25 18:46:17 -08001394 }
1395
Hans Boehm8f051c32016-10-03 16:53:58 -07001396 private void clearIfNotInputState() {
1397 if (mCurrentState == CalculatorState.ERROR
1398 || mCurrentState == CalculatorState.RESULT) {
1399 setState(CalculatorState.INPUT);
1400 mEvaluator.clearMain();
1401 }
1402 }
1403
Annie Chind0f87d22016-10-24 09:04:12 -07001404 private boolean isViewTarget(View view, MotionEvent event) {
1405 mHitRect.set(0, 0, view.getWidth(), view.getHeight());
1406 mDragLayout.offsetDescendantRectToMyCoords(view, mHitRect);
1407 return mHitRect.contains((int) event.getX(), (int) event.getY());
1408 }
1409
Chenjie Yu3937b652016-06-01 23:14:26 -07001410 /**
Christine Franksbd90b792016-11-22 10:28:26 -08001411 * Since we only support LTR format, using the RTL comma does not make sense.
1412 */
1413 private String getDecimalSeparator() {
1414 final char defaultSeparator = DecimalFormatSymbols.getInstance().getDecimalSeparator();
1415 final char rtlComma = '\u066b';
1416 return defaultSeparator == rtlComma ? "," : String.valueOf(defaultSeparator);
1417 }
1418
1419 /**
Chenjie Yu3937b652016-06-01 23:14:26 -07001420 * Clean up animation for context menu.
1421 */
1422 @Override
1423 public void onContextMenuClosed(Menu menu) {
1424 stopActionModeOrContextMenu();
1425 }
Christine Franks1d99be12016-11-14 14:00:36 -08001426
1427 public interface OnDisplayMemoryOperationsListener {
1428 boolean shouldDisplayMemory();
1429 }
Justin Klaassen4b3af052014-05-27 17:53:10 -07001430}