blob: 3a4ac6df8bd28b560f00ff1c7a7c47347afba6c6 [file] [log] [blame]
Justin Klaassen4b3af052014-05-27 17:53:10 -07001/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
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 Boehm4a6b7cb2015-04-03 18:41:52 -070017// FIXME: Menu handling, particularly for cut/paste, is very ugly
18// and not the way it was intended.
19// Other menus are not handled brilliantly either.
20// TODO: Revisit handling of "Help" menu, so that it's more consistent
21// with our conventions.
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070022// TODO: Better indication of when the result is known to be exact.
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070023// TODO: Check and possibly fix accessability issues.
Hans Boehm013969e2015-04-13 20:29:47 -070024// TODO: Copy & more general paste in formula? Note that this requires
25// great care: Currently the text version of a displayed formula
26// is not directly useful for re-evaluating the formula later, since
27// it contains ellipses representing subexpressions evaluated with
28// a different degree mode. Rather than supporting copy from the
29// formula window, we may eventually want to support generation of a
30// more useful text version in a separate window. It's not clear
31// this is worth the added (code and user) complexity.
Hans Boehm84614952014-11-25 18:46:17 -080032
Justin Klaassen4b3af052014-05-27 17:53:10 -070033package com.android.calculator2;
34
35import android.animation.Animator;
Justin Klaassen5f2a3342014-06-11 17:40:22 -070036import android.animation.Animator.AnimatorListener;
Justin Klaassen4b3af052014-05-27 17:53:10 -070037import android.animation.AnimatorListenerAdapter;
38import android.animation.AnimatorSet;
Justin Klaassen4b3af052014-05-27 17:53:10 -070039import android.animation.ObjectAnimator;
Justin Klaassen4b3af052014-05-27 17:53:10 -070040import android.app.Activity;
Hans Boehm84614952014-11-25 18:46:17 -080041import android.app.AlertDialog;
Hans Boehm84614952014-11-25 18:46:17 -080042import android.content.DialogInterface;
Justin Klaassend36d63e2015-05-05 12:59:36 -070043import android.content.Intent;
Hans Boehmbfe8c222015-04-02 16:26:07 -070044import android.content.res.Resources;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070045import android.graphics.Color;
Justin Klaassen8fff1442014-06-19 10:43:29 -070046import android.graphics.Rect;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070047import android.net.Uri;
Justin Klaassen4b3af052014-05-27 17:53:10 -070048import android.os.Bundle;
Justin Klaassenf79d6f62014-08-26 12:27:08 -070049import android.support.annotation.NonNull;
Justin Klaassen3b4d13d2014-06-06 18:18:37 +010050import android.support.v4.view.ViewPager;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070051import android.text.SpannableString;
52import android.text.Spanned;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070053import android.text.style.ForegroundColorSpan;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070054import android.view.KeyCharacterMap;
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -070055import android.view.KeyEvent;
Hans Boehm84614952014-11-25 18:46:17 -080056import android.view.Menu;
57import android.view.MenuItem;
Justin Klaassen4b3af052014-05-27 17:53:10 -070058import android.view.View;
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -070059import android.view.View.OnKeyListener;
Justin Klaassen4b3af052014-05-27 17:53:10 -070060import android.view.View.OnLongClickListener;
Justin Klaassen5f2a3342014-06-11 17:40:22 -070061import android.view.ViewAnimationUtils;
Justin Klaassen8fff1442014-06-19 10:43:29 -070062import android.view.ViewGroupOverlay;
Justin Klaassen4b3af052014-05-27 17:53:10 -070063import android.view.animation.AccelerateDecelerateInterpolator;
Justin Klaassenfed941a2014-06-09 18:42:40 +010064import android.widget.TextView;
Justin Klaassend48b7562015-04-16 16:51:38 -070065import android.widget.Toolbar;
Justin Klaassenfed941a2014-06-09 18:42:40 +010066
Hans Boehm08e8f322015-04-21 13:18:38 -070067import com.android.calculator2.CalculatorText.OnTextSizeChangeListener;
Hans Boehm84614952014-11-25 18:46:17 -080068
69import java.io.ByteArrayInputStream;
Hans Boehm84614952014-11-25 18:46:17 -080070import java.io.ByteArrayOutputStream;
Hans Boehm84614952014-11-25 18:46:17 -080071import java.io.IOException;
Justin Klaassen721ec842015-05-28 14:30:08 -070072import java.io.ObjectInput;
73import java.io.ObjectInputStream;
74import java.io.ObjectOutput;
75import java.io.ObjectOutputStream;
Justin Klaassen4b3af052014-05-27 17:53:10 -070076
Justin Klaassen04f79c72014-06-27 17:25:35 -070077public class Calculator extends Activity
Hans Boehm08e8f322015-04-21 13:18:38 -070078 implements OnTextSizeChangeListener, OnLongClickListener, CalculatorText.PasteListener {
Justin Klaassen2be4fdb2014-08-06 19:54:09 -070079
80 /**
81 * Constant for an invalid resource id.
82 */
83 public static final int INVALID_RES_ID = -1;
Justin Klaassen4b3af052014-05-27 17:53:10 -070084
85 private enum CalculatorState {
Hans Boehm84614952014-11-25 18:46:17 -080086 INPUT, // Result and formula both visible, no evaluation requested,
87 // Though result may be visible on bottom line.
88 EVALUATE, // Both visible, evaluation requested, evaluation/animation incomplete.
89 INIT, // Very temporary state used as alternative to EVALUATE
90 // during reinitialization. Do not animate on completion.
91 ANIMATE, // Result computed, animation to enlarge result window in progress.
92 RESULT, // Result displayed, formula invisible.
93 // If we are in RESULT state, the formula was evaluated without
94 // error to initial precision.
95 ERROR // Error displayed: Formula visible, result shows error message.
96 // Display similar to INPUT state.
Justin Klaassen4b3af052014-05-27 17:53:10 -070097 }
Hans Boehm84614952014-11-25 18:46:17 -080098 // Normal transition sequence is
99 // INPUT -> EVALUATE -> ANIMATE -> RESULT (or ERROR) -> INPUT
100 // A RESULT -> ERROR transition is possible in rare corner cases, in which
101 // a higher precision evaluation exposes an error. This is possible, since we
102 // initially evaluate assuming we were given a well-defined problem. If we
103 // were actually asked to compute sqrt(<extremely tiny negative number>) we produce 0
104 // unless we are asked for enough precision that we can distinguish the argument from zero.
105 // TODO: Consider further heuristics to reduce the chance of observing this?
106 // It already seems to be observable only in contrived cases.
107 // ANIMATE, ERROR, and RESULT are translated to an INIT state if the application
108 // is restarted in that state. This leads us to recompute and redisplay the result
109 // ASAP.
110 // TODO: Possibly save a bit more information, e.g. its initial display string
111 // or most significant digit position, to speed up restart.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700112
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700113 // We currently assume that the formula does not change out from under us in
114 // any way. We explicitly handle all input to the formula here.
115 // TODO: Perhaps the formula should not be editable at all?
Justin Klaassen4b3af052014-05-27 17:53:10 -0700116
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700117 private final OnKeyListener mFormulaOnKeyListener = new OnKeyListener() {
118 @Override
119 public boolean onKey(View view, int keyCode, KeyEvent keyEvent) {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700120 if (keyEvent.getAction() != KeyEvent.ACTION_UP) return true;
Hans Boehm1176f232015-05-11 16:26:03 -0700121 stopActionMode();
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700122 switch (keyCode) {
123 case KeyEvent.KEYCODE_NUMPAD_ENTER:
124 case KeyEvent.KEYCODE_ENTER:
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700125 case KeyEvent.KEYCODE_DPAD_CENTER:
126 mCurrentButton = mEqualButton;
127 onEquals();
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700128 return true;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700129 case KeyEvent.KEYCODE_DEL:
130 mCurrentButton = mDeleteButton;
131 onDelete();
132 return true;
133 default:
134 final int raw = keyEvent.getKeyCharacterMap()
135 .get(keyCode, keyEvent.getMetaState());
136 if ((raw & KeyCharacterMap.COMBINING_ACCENT) != 0) {
137 return true; // discard
138 }
139 // Try to discard non-printing characters and the like.
140 // The user will have to explicitly delete other junk that gets past us.
141 if (Character.isIdentifierIgnorable(raw)
142 || Character.isWhitespace(raw)) {
143 return true;
144 }
145 char c = (char)raw;
146 if (c == '=') {
Hans Boehme57fb012015-05-07 19:52:32 -0700147 mCurrentButton = mEqualButton;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700148 onEquals();
149 } else {
150 addChars(String.valueOf(c));
151 redisplayAfterFormulaChange();
152 }
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700153 }
154 return false;
155 }
156 };
157
Hans Boehm84614952014-11-25 18:46:17 -0800158 private static final String NAME = Calculator.class.getName();
159 private static final String KEY_DISPLAY_STATE = NAME + "_display_state";
Hans Boehm760a9dc2015-04-20 10:27:12 -0700160 private static final String KEY_UNPROCESSED_CHARS = NAME + "_unprocessed_chars";
Hans Boehm84614952014-11-25 18:46:17 -0800161 private static final String KEY_EVAL_STATE = NAME + "_eval_state";
162 // Associated value is a byte array holding both mCalculatorState
163 // and the (much more complex) evaluator state.
Justin Klaassen741471e2014-06-11 09:43:44 -0700164
Justin Klaassen4b3af052014-05-27 17:53:10 -0700165 private CalculatorState mCurrentState;
Hans Boehm84614952014-11-25 18:46:17 -0800166 private Evaluator mEvaluator;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700167
Justin Klaassen06360f92014-08-28 11:08:44 -0700168 private View mDisplayView;
Justin Klaassend48b7562015-04-16 16:51:38 -0700169 private TextView mModeView;
Hans Boehm08e8f322015-04-21 13:18:38 -0700170 private CalculatorText mFormulaText;
Hans Boehm84614952014-11-25 18:46:17 -0800171 private CalculatorResult mResult;
Justin Klaassend48b7562015-04-16 16:51:38 -0700172
Justin Klaassen3b4d13d2014-06-06 18:18:37 +0100173 private ViewPager mPadViewPager;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700174 private View mDeleteButton;
175 private View mClearButton;
Justin Klaassend48b7562015-04-16 16:51:38 -0700176 private View mEqualButton;
Justin Klaassene2711cb2015-05-28 11:13:17 -0700177
178 private TextView mInverseToggle;
179 private TextView mModeToggle;
180
Justin Klaassen721ec842015-05-28 14:30:08 -0700181 private View[] mInvertibleButtons;
Justin Klaassene2711cb2015-05-28 11:13:17 -0700182 private View[] mInverseButtons;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700183
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700184 private View mCurrentButton;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700185 private Animator mCurrentAnimator;
186
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700187 private String mUnprocessedChars = null; // Characters that were recently entered
188 // at the end of the display that have not yet
189 // been added to the underlying expression.
190
Justin Klaassen4b3af052014-05-27 17:53:10 -0700191 @Override
192 protected void onCreate(Bundle savedInstanceState) {
193 super.onCreate(savedInstanceState);
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700194 setContentView(R.layout.activity_calculator);
Justin Klaassend48b7562015-04-16 16:51:38 -0700195 setActionBar((Toolbar) findViewById(R.id.toolbar));
196
197 // Hide all default options in the ActionBar.
198 getActionBar().setDisplayOptions(0);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700199
Justin Klaassen06360f92014-08-28 11:08:44 -0700200 mDisplayView = findViewById(R.id.display);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700201 mModeView = (TextView) findViewById(R.id.mode);
Hans Boehm08e8f322015-04-21 13:18:38 -0700202 mFormulaText = (CalculatorText) findViewById(R.id.formula);
Hans Boehm84614952014-11-25 18:46:17 -0800203 mResult = (CalculatorResult) findViewById(R.id.result);
Justin Klaassend48b7562015-04-16 16:51:38 -0700204
Justin Klaassen3b4d13d2014-06-06 18:18:37 +0100205 mPadViewPager = (ViewPager) findViewById(R.id.pad_pager);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700206 mDeleteButton = findViewById(R.id.del);
207 mClearButton = findViewById(R.id.clr);
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700208 mEqualButton = findViewById(R.id.pad_numeric).findViewById(R.id.eq);
209 if (mEqualButton == null || mEqualButton.getVisibility() != View.VISIBLE) {
210 mEqualButton = findViewById(R.id.pad_operator).findViewById(R.id.eq);
211 }
Justin Klaassene2711cb2015-05-28 11:13:17 -0700212
213 mInverseToggle = (TextView) findViewById(R.id.toggle_inv);
214 mModeToggle = (TextView) findViewById(R.id.toggle_mode);
215
Justin Klaassen721ec842015-05-28 14:30:08 -0700216 mInvertibleButtons = new View[] {
217 findViewById(R.id.fun_sin),
218 findViewById(R.id.fun_cos),
219 findViewById(R.id.fun_tan)
Justin Klaassene2711cb2015-05-28 11:13:17 -0700220 };
221 mInverseButtons = new View[] {
222 findViewById(R.id.fun_arcsin),
223 findViewById(R.id.fun_arccos),
224 findViewById(R.id.fun_arctan)
225 };
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700226
Hans Boehm84614952014-11-25 18:46:17 -0800227 mEvaluator = new Evaluator(this, mResult);
228 mResult.setEvaluator(mEvaluator);
Hans Boehm013969e2015-04-13 20:29:47 -0700229 KeyMaps.setActivity(this);
Justin Klaassen2be4fdb2014-08-06 19:54:09 -0700230
Hans Boehm84614952014-11-25 18:46:17 -0800231 if (savedInstanceState != null) {
232 setState(CalculatorState.values()[
233 savedInstanceState.getInt(KEY_DISPLAY_STATE,
234 CalculatorState.INPUT.ordinal())]);
Hans Boehm760a9dc2015-04-20 10:27:12 -0700235 CharSequence unprocessed = savedInstanceState.getCharSequence(KEY_UNPROCESSED_CHARS);
236 if (unprocessed != null) {
237 mUnprocessedChars = unprocessed.toString();
238 }
239 byte[] state = savedInstanceState.getByteArray(KEY_EVAL_STATE);
Hans Boehm84614952014-11-25 18:46:17 -0800240 if (state != null) {
241 try (ObjectInput in = new ObjectInputStream(new ByteArrayInputStream(state))) {
242 mEvaluator.restoreInstanceState(in);
243 } catch (Throwable ignored) {
244 // When in doubt, revert to clean state
245 mCurrentState = CalculatorState.INPUT;
246 mEvaluator.clear();
247 }
248 }
Hans Boehmfbcef702015-04-27 18:07:47 -0700249 } else {
250 mCurrentState = CalculatorState.INPUT;
251 mEvaluator.clear();
Hans Boehm84614952014-11-25 18:46:17 -0800252 }
Justin Klaassene2711cb2015-05-28 11:13:17 -0700253
Hans Boehm08e8f322015-04-21 13:18:38 -0700254 mFormulaText.setOnKeyListener(mFormulaOnKeyListener);
255 mFormulaText.setOnTextSizeChangeListener(this);
256 mFormulaText.setPasteListener(this);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700257 mDeleteButton.setOnLongClickListener(this);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700258
259 onInverseToggled(mInverseToggle.isSelected());
260 onModeChanged(mEvaluator.getDegreeMode());
261
Hans Boehm84614952014-11-25 18:46:17 -0800262 if (mCurrentState != CalculatorState.INPUT) {
Hans Boehmfbcef702015-04-27 18:07:47 -0700263 // Just reevaluate.
264 redisplayFormula();
Hans Boehm84614952014-11-25 18:46:17 -0800265 setState(CalculatorState.INIT);
Hans Boehm84614952014-11-25 18:46:17 -0800266 mEvaluator.requireResult();
267 } else {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700268 redisplayAfterFormulaChange();
Hans Boehm84614952014-11-25 18:46:17 -0800269 }
270 // TODO: We're currently not saving and restoring scroll position.
271 // We probably should. Details may require care to deal with:
272 // - new display size
273 // - slow recomputation if we've scrolled far.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700274 }
275
276 @Override
Justin Klaassenf79d6f62014-08-26 12:27:08 -0700277 protected void onSaveInstanceState(@NonNull Bundle outState) {
278 // If there's an animation in progress, cancel it first to ensure our state is up-to-date.
279 if (mCurrentAnimator != null) {
280 mCurrentAnimator.cancel();
281 }
282
Justin Klaassen4b3af052014-05-27 17:53:10 -0700283 super.onSaveInstanceState(outState);
Hans Boehm84614952014-11-25 18:46:17 -0800284 outState.putInt(KEY_DISPLAY_STATE, mCurrentState.ordinal());
Hans Boehm760a9dc2015-04-20 10:27:12 -0700285 outState.putCharSequence(KEY_UNPROCESSED_CHARS, mUnprocessedChars);
Hans Boehm84614952014-11-25 18:46:17 -0800286 ByteArrayOutputStream byteArrayStream = new ByteArrayOutputStream();
287 try (ObjectOutput out = new ObjectOutputStream(byteArrayStream)) {
288 mEvaluator.saveInstanceState(out);
289 } catch (IOException e) {
290 // Impossible; No IO involved.
291 throw new AssertionError("Impossible IO exception", e);
292 }
293 outState.putByteArray(KEY_EVAL_STATE, byteArrayStream.toByteArray());
Justin Klaassen4b3af052014-05-27 17:53:10 -0700294 }
295
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700296 // Set the state, updating delete label and display colors.
297 // This restores display positions on moving to INPUT.
Justin Klaassend48b7562015-04-16 16:51:38 -0700298 // But movement/animation for moving to RESULT has already been done.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700299 private void setState(CalculatorState state) {
300 if (mCurrentState != state) {
Hans Boehm84614952014-11-25 18:46:17 -0800301 if (state == CalculatorState.INPUT) {
302 restoreDisplayPositions();
303 }
Justin Klaassen4b3af052014-05-27 17:53:10 -0700304 mCurrentState = state;
305
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700306 if (mCurrentState == CalculatorState.RESULT) {
307 // No longer do this for ERROR; allow mistakes to be corrected.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700308 mDeleteButton.setVisibility(View.GONE);
309 mClearButton.setVisibility(View.VISIBLE);
310 } else {
311 mDeleteButton.setVisibility(View.VISIBLE);
312 mClearButton.setVisibility(View.GONE);
313 }
314
Hans Boehm84614952014-11-25 18:46:17 -0800315 if (mCurrentState == CalculatorState.ERROR) {
Justin Klaassen4b3af052014-05-27 17:53:10 -0700316 final int errorColor = getResources().getColor(R.color.calculator_error_color);
Hans Boehm08e8f322015-04-21 13:18:38 -0700317 mFormulaText.setTextColor(errorColor);
Hans Boehm84614952014-11-25 18:46:17 -0800318 mResult.setTextColor(errorColor);
Justin Klaassen8fff1442014-06-19 10:43:29 -0700319 getWindow().setStatusBarColor(errorColor);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700320 } else {
Hans Boehm08e8f322015-04-21 13:18:38 -0700321 mFormulaText.setTextColor(
Justin Klaassen4b3af052014-05-27 17:53:10 -0700322 getResources().getColor(R.color.display_formula_text_color));
Hans Boehm84614952014-11-25 18:46:17 -0800323 mResult.setTextColor(
Justin Klaassen4b3af052014-05-27 17:53:10 -0700324 getResources().getColor(R.color.display_result_text_color));
Justin Klaassen8fff1442014-06-19 10:43:29 -0700325 getWindow().setStatusBarColor(
Justin Klaassen4b3af052014-05-27 17:53:10 -0700326 getResources().getColor(R.color.calculator_accent_color));
327 }
Justin Klaassend48b7562015-04-16 16:51:38 -0700328
329 invalidateOptionsMenu();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700330 }
331 }
332
Hans Boehm1176f232015-05-11 16:26:03 -0700333 // Stop any active ActionMode. Return true if there was one.
334 private boolean stopActionMode() {
335 if (mResult.stopActionMode()) {
336 return true;
337 }
338 if (mFormulaText.stopActionMode()) {
339 return true;
340 }
341 return false;
342 }
343
Justin Klaassen4b3af052014-05-27 17:53:10 -0700344 @Override
Justin Klaassen3b4d13d2014-06-06 18:18:37 +0100345 public void onBackPressed() {
Hans Boehm1176f232015-05-11 16:26:03 -0700346 if (!stopActionMode()) {
347 if (mPadViewPager != null && mPadViewPager.getCurrentItem() != 0) {
348 // Select the previous pad.
349 mPadViewPager.setCurrentItem(mPadViewPager.getCurrentItem() - 1);
350 } else {
351 // If the user is currently looking at the first pad (or the pad is not paged),
352 // allow the system to handle the Back button.
353 super.onBackPressed();
354 }
Justin Klaassen3b4d13d2014-06-06 18:18:37 +0100355 }
356 }
357
358 @Override
Justin Klaassen4b3af052014-05-27 17:53:10 -0700359 public void onUserInteraction() {
360 super.onUserInteraction();
361
362 // If there's an animation in progress, cancel it so the user interaction can be handled
363 // immediately.
364 if (mCurrentAnimator != null) {
365 mCurrentAnimator.cancel();
366 }
367 }
368
Justin Klaassene2711cb2015-05-28 11:13:17 -0700369 /**
370 * Invoked whenever the inverse button is toggled to update the UI.
371 *
372 * @param showInverse {@code true} if inverse functions should be shown
373 */
374 private void onInverseToggled(boolean showInverse) {
375 if (showInverse) {
376 mInverseToggle.setContentDescription(getString(R.string.desc_inv_on));
Justin Klaassen721ec842015-05-28 14:30:08 -0700377 for (View invertibleButton : mInvertibleButtons) {
378 invertibleButton.setVisibility(View.GONE);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700379 }
380 for (View inverseButton : mInverseButtons) {
381 inverseButton.setVisibility(View.VISIBLE);
382 }
383 } else {
384 mInverseToggle.setContentDescription(getString(R.string.desc_inv_off));
Justin Klaassen721ec842015-05-28 14:30:08 -0700385 for (View invertibleButton : mInvertibleButtons) {
386 invertibleButton.setVisibility(View.VISIBLE);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700387 }
388 for (View inverseButton : mInverseButtons) {
389 inverseButton.setVisibility(View.GONE);
390 }
391 }
392 }
393
394 /**
395 * Invoked whenever the deg/rad mode may have changed to update the UI.
396 *
397 * @param degreeMode {@code true} if in degree mode
398 */
399 private void onModeChanged(boolean degreeMode) {
400 if (degreeMode) {
Justin Klaassend48b7562015-04-16 16:51:38 -0700401 mModeView.setText(R.string.mode_deg);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700402 mModeView.setContentDescription(getString(R.string.desc_mode_deg));
403
404 mModeToggle.setText(R.string.mode_rad);
405 mModeToggle.setContentDescription(getString(R.string.desc_switch_rad));
Hans Boehmbfe8c222015-04-02 16:26:07 -0700406 } else {
Justin Klaassend48b7562015-04-16 16:51:38 -0700407 mModeView.setText(R.string.mode_rad);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700408 mModeView.setContentDescription(getString(R.string.desc_mode_rad));
409
410 mModeToggle.setText(R.string.mode_deg);
411 mModeToggle.setContentDescription(getString(R.string.desc_switch_deg));
Hans Boehmbfe8c222015-04-02 16:26:07 -0700412 }
413 }
Hans Boehm84614952014-11-25 18:46:17 -0800414
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700415 // Add the given button id to input expression.
416 // If appropriate, clear the expression before doing so.
417 private void addKeyToExpr(int id) {
418 if (mCurrentState == CalculatorState.ERROR) {
419 setState(CalculatorState.INPUT);
420 } else if (mCurrentState == CalculatorState.RESULT) {
421 if (KeyMaps.isBinary(id) || KeyMaps.isSuffix(id)) {
422 mEvaluator.collapse();
423 } else {
424 mEvaluator.clear();
425 }
426 setState(CalculatorState.INPUT);
427 }
428 if (!mEvaluator.append(id)) {
429 // TODO: Some user visible feedback?
430 }
431 }
432
433 private void redisplayAfterFormulaChange() {
434 // TODO: Could do this more incrementally.
435 redisplayFormula();
436 setState(CalculatorState.INPUT);
Hans Boehmc023b732015-04-29 11:30:47 -0700437 if (mEvaluator.getExpr().hasInterestingOps()) {
438 mEvaluator.evaluateAndShowResult();
439 } else {
440 mResult.clear();
441 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700442 }
443
Justin Klaassen4b3af052014-05-27 17:53:10 -0700444 public void onButtonClick(View view) {
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700445 mCurrentButton = view;
Hans Boehm1176f232015-05-11 16:26:03 -0700446 stopActionMode();
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700447
Hans Boehm84614952014-11-25 18:46:17 -0800448 // Always cancel in-progress evaluation.
449 // If we were waiting for the result, do nothing else.
450 mEvaluator.cancelAll();
Justin Klaassend48b7562015-04-16 16:51:38 -0700451
Hans Boehm84614952014-11-25 18:46:17 -0800452 if (mCurrentState == CalculatorState.EVALUATE
453 || mCurrentState == CalculatorState.ANIMATE) {
454 onCancelled();
455 return;
456 }
Justin Klaassend48b7562015-04-16 16:51:38 -0700457
Justin Klaassend48b7562015-04-16 16:51:38 -0700458 final int id = view.getId();
Hans Boehm84614952014-11-25 18:46:17 -0800459 switch (id) {
Justin Klaassen4b3af052014-05-27 17:53:10 -0700460 case R.id.eq:
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700461 onEquals();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700462 break;
463 case R.id.del:
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700464 onDelete();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700465 break;
466 case R.id.clr:
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700467 onClear();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700468 break;
Justin Klaassene2711cb2015-05-28 11:13:17 -0700469 case R.id.toggle_inv:
470 final boolean selected = !mInverseToggle.isSelected();
471 mInverseToggle.setSelected(selected);
472 onInverseToggled(selected);
473 break;
474 case R.id.toggle_mode:
475 final boolean mode = !mEvaluator.getDegreeMode();
Hans Boehmbfe8c222015-04-02 16:26:07 -0700476 if (mCurrentState == CalculatorState.RESULT) {
477 mEvaluator.collapse(); // Capture result evaluated in old mode
478 redisplayFormula();
479 }
480 // In input mode, we reinterpret already entered trig functions.
481 mEvaluator.setDegreeMode(mode);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700482 onModeChanged(mode);
483
Hans Boehmbfe8c222015-04-02 16:26:07 -0700484 setState(CalculatorState.INPUT);
485 mResult.clear();
Hans Boehmc023b732015-04-29 11:30:47 -0700486 if (mEvaluator.getExpr().hasInterestingOps()) {
487 mEvaluator.evaluateAndShowResult();
488 }
Hans Boehmbfe8c222015-04-02 16:26:07 -0700489 break;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700490 default:
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700491 addKeyToExpr(id);
492 redisplayAfterFormulaChange();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700493 break;
494 }
495 }
496
Hans Boehm84614952014-11-25 18:46:17 -0800497 void redisplayFormula() {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700498 String formula = mEvaluator.getExpr().toString(this);
499 if (mUnprocessedChars != null) {
500 // Add and highlight characters we couldn't process.
501 SpannableString formatted = new SpannableString(formula + mUnprocessedChars);
502 // TODO: should probably match this to the error color.
503 formatted.setSpan(new ForegroundColorSpan(Color.RED),
504 formula.length(), formatted.length(),
505 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
Hans Boehm08e8f322015-04-21 13:18:38 -0700506 mFormulaText.setText(formatted);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700507 } else {
Hans Boehm08e8f322015-04-21 13:18:38 -0700508 mFormulaText.setText(formula);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700509 }
Hans Boehm84614952014-11-25 18:46:17 -0800510 }
511
Justin Klaassen4b3af052014-05-27 17:53:10 -0700512 @Override
513 public boolean onLongClick(View view) {
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700514 mCurrentButton = view;
515
Justin Klaassen4b3af052014-05-27 17:53:10 -0700516 if (view.getId() == R.id.del) {
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700517 onClear();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700518 return true;
519 }
520 return false;
521 }
522
Hans Boehm84614952014-11-25 18:46:17 -0800523 // Initial evaluation completed successfully. Initiate display.
Hans Boehm61568a12015-05-18 18:25:41 -0700524 public void onEvaluate(int initDisplayPrec, int leastDigPos, String truncatedWholeNumber) {
Justin Klaassend48b7562015-04-16 16:51:38 -0700525 // Invalidate any options that may depend on the current result.
526 invalidateOptionsMenu();
527
Hans Boehm61568a12015-05-18 18:25:41 -0700528 mResult.displayResult(initDisplayPrec, leastDigPos, truncatedWholeNumber);
529 if (mCurrentState != CalculatorState.INPUT) { // in EVALUATE or INIT state
Hans Boehm84614952014-11-25 18:46:17 -0800530 onResult(mCurrentState != CalculatorState.INIT);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700531 }
Hans Boehm84614952014-11-25 18:46:17 -0800532 }
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700533
Hans Boehm84614952014-11-25 18:46:17 -0800534 public void onCancelled() {
535 // We should be in EVALUATE state.
536 // Display is still in input state.
537 setState(CalculatorState.INPUT);
Hans Boehmc023b732015-04-29 11:30:47 -0700538 mResult.clear();
Hans Boehm84614952014-11-25 18:46:17 -0800539 }
540
541 // Reevaluation completed; ask result to redisplay current value.
542 public void onReevaluate()
543 {
544 mResult.redisplay();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700545 }
546
Justin Klaassenfed941a2014-06-09 18:42:40 +0100547 @Override
548 public void onTextSizeChanged(final TextView textView, float oldSize) {
549 if (mCurrentState != CalculatorState.INPUT) {
550 // Only animate text changes that occur from user input.
551 return;
552 }
553
554 // Calculate the values needed to perform the scale and translation animations,
555 // maintaining the same apparent baseline for the displayed text.
556 final float textScale = oldSize / textView.getTextSize();
557 final float translationX = (1.0f - textScale) *
558 (textView.getWidth() / 2.0f - textView.getPaddingEnd());
559 final float translationY = (1.0f - textScale) *
560 (textView.getHeight() / 2.0f - textView.getPaddingBottom());
561
562 final AnimatorSet animatorSet = new AnimatorSet();
563 animatorSet.playTogether(
564 ObjectAnimator.ofFloat(textView, View.SCALE_X, textScale, 1.0f),
565 ObjectAnimator.ofFloat(textView, View.SCALE_Y, textScale, 1.0f),
566 ObjectAnimator.ofFloat(textView, View.TRANSLATION_X, translationX, 0.0f),
567 ObjectAnimator.ofFloat(textView, View.TRANSLATION_Y, translationY, 0.0f));
Justin Klaassen94db7202014-06-11 11:22:31 -0700568 animatorSet.setDuration(getResources().getInteger(android.R.integer.config_mediumAnimTime));
Justin Klaassenfed941a2014-06-09 18:42:40 +0100569 animatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
570 animatorSet.start();
571 }
572
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700573 private void onEquals() {
Hans Boehmc023b732015-04-29 11:30:47 -0700574 if (mCurrentState == CalculatorState.INPUT && !mEvaluator.getExpr().isEmpty()) {
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700575 setState(CalculatorState.EVALUATE);
Hans Boehm84614952014-11-25 18:46:17 -0800576 mEvaluator.requireResult();
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700577 }
578 }
579
580 private void onDelete() {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700581 // Delete works like backspace; remove the last character or operator from the expression.
582 // Note that we handle keyboard delete exactly like the delete button. For
583 // example the delete button can be used to delete a character from an incomplete
584 // function name typed on a physical keyboard.
Hans Boehm84614952014-11-25 18:46:17 -0800585 mEvaluator.cancelAll();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700586 // This should be impossible in RESULT state.
587 setState(CalculatorState.INPUT);
588 if (mUnprocessedChars != null) {
589 int len = mUnprocessedChars.length();
590 if (len > 0) {
591 mUnprocessedChars = mUnprocessedChars.substring(0, len-1);
592 } else {
Hans Boehmc023b732015-04-29 11:30:47 -0700593 mEvaluator.delete();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700594 }
595 } else {
Hans Boehmc023b732015-04-29 11:30:47 -0700596 mEvaluator.delete();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700597 }
598 redisplayAfterFormulaChange();
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700599 }
600
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700601 private void reveal(View sourceView, int colorRes, AnimatorListener listener) {
Justin Klaassen06360f92014-08-28 11:08:44 -0700602 final ViewGroupOverlay groupOverlay =
603 (ViewGroupOverlay) getWindow().getDecorView().getOverlay();
Justin Klaassen8fff1442014-06-19 10:43:29 -0700604
605 final Rect displayRect = new Rect();
Justin Klaassen06360f92014-08-28 11:08:44 -0700606 mDisplayView.getGlobalVisibleRect(displayRect);
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700607
608 // Make reveal cover the display and status bar.
609 final View revealView = new View(this);
Justin Klaassen8fff1442014-06-19 10:43:29 -0700610 revealView.setBottom(displayRect.bottom);
611 revealView.setLeft(displayRect.left);
612 revealView.setRight(displayRect.right);
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700613 revealView.setBackgroundColor(getResources().getColor(colorRes));
Justin Klaassen06360f92014-08-28 11:08:44 -0700614 groupOverlay.add(revealView);
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700615
Justin Klaassen4b3af052014-05-27 17:53:10 -0700616 final int[] clearLocation = new int[2];
617 sourceView.getLocationInWindow(clearLocation);
618 clearLocation[0] += sourceView.getWidth() / 2;
619 clearLocation[1] += sourceView.getHeight() / 2;
620
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700621 final int revealCenterX = clearLocation[0] - revealView.getLeft();
622 final int revealCenterY = clearLocation[1] - revealView.getTop();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700623
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700624 final double x1_2 = Math.pow(revealView.getLeft() - revealCenterX, 2);
625 final double x2_2 = Math.pow(revealView.getRight() - revealCenterX, 2);
626 final double y_2 = Math.pow(revealView.getTop() - revealCenterY, 2);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700627 final float revealRadius = (float) Math.max(Math.sqrt(x1_2 + y_2), Math.sqrt(x2_2 + y_2));
628
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700629 final Animator revealAnimator =
630 ViewAnimationUtils.createCircularReveal(revealView,
ztenghui3d6ecaf2014-06-05 09:56:00 -0700631 revealCenterX, revealCenterY, 0.0f, revealRadius);
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700632 revealAnimator.setDuration(
Justin Klaassen4b3af052014-05-27 17:53:10 -0700633 getResources().getInteger(android.R.integer.config_longAnimTime));
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700634 revealAnimator.addListener(listener);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700635
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700636 final Animator alphaAnimator = ObjectAnimator.ofFloat(revealView, View.ALPHA, 0.0f);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700637 alphaAnimator.setDuration(
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700638 getResources().getInteger(android.R.integer.config_mediumAnimTime));
Justin Klaassen4b3af052014-05-27 17:53:10 -0700639
640 final AnimatorSet animatorSet = new AnimatorSet();
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700641 animatorSet.play(revealAnimator).before(alphaAnimator);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700642 animatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
643 animatorSet.addListener(new AnimatorListenerAdapter() {
644 @Override
Justin Klaassen4b3af052014-05-27 17:53:10 -0700645 public void onAnimationEnd(Animator animator) {
Justin Klaassen8fff1442014-06-19 10:43:29 -0700646 groupOverlay.remove(revealView);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700647 mCurrentAnimator = null;
648 }
649 });
650
651 mCurrentAnimator = animatorSet;
652 animatorSet.start();
653 }
654
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700655 private void onClear() {
Hans Boehm84614952014-11-25 18:46:17 -0800656 if (mEvaluator.getExpr().isEmpty()) {
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700657 return;
658 }
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700659 reveal(mCurrentButton, R.color.calculator_accent_color, new AnimatorListenerAdapter() {
660 @Override
661 public void onAnimationEnd(Animator animation) {
Hans Boehm760a9dc2015-04-20 10:27:12 -0700662 mUnprocessedChars = null;
663 mResult.clear();
664 mEvaluator.clear();
665 setState(CalculatorState.INPUT);
Hans Boehm84614952014-11-25 18:46:17 -0800666 redisplayFormula();
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700667 }
668 });
669 }
670
Hans Boehm84614952014-11-25 18:46:17 -0800671 // Evaluation encountered en error. Display the error.
672 void onError(final int errorResourceId) {
Hans Boehmfbcef702015-04-27 18:07:47 -0700673 if (mCurrentState == CalculatorState.EVALUATE) {
674 setState(CalculatorState.ANIMATE);
675 reveal(mCurrentButton, R.color.calculator_error_color,
676 new AnimatorListenerAdapter() {
677 @Override
678 public void onAnimationEnd(Animator animation) {
679 setState(CalculatorState.ERROR);
680 mResult.displayError(errorResourceId);
681 }
682 });
683 } else if (mCurrentState == CalculatorState.INIT) {
684 setState(CalculatorState.ERROR);
685 mResult.displayError(errorResourceId);
Hans Boehmc023b732015-04-29 11:30:47 -0700686 } else {
687 mResult.clear();
Justin Klaassen2be4fdb2014-08-06 19:54:09 -0700688 }
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700689 }
690
Hans Boehm84614952014-11-25 18:46:17 -0800691
692 // Animate movement of result into the top formula slot.
693 // Result window now remains translated in the top slot while the result is displayed.
694 // (We convert it back to formula use only when the user provides new input.)
695 // Historical note: In the Lollipop version, this invisibly and instantaeously moved
696 // formula and result displays back at the end of the animation. We no longer do that,
697 // so that we can continue to properly support scrolling of the result.
698 // We assume the result already contains the text to be expanded.
699 private void onResult(boolean animate) {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700700 // Calculate the values needed to perform the scale and translation animations.
Hans Boehm61568a12015-05-18 18:25:41 -0700701 // The nominal font size in the result display is fixed. But the magnification we
702 // use when the user hits "=" is variable, with a scrollable result always getting
703 // minimum magnification.
704 // Display.xml is designed to ensure that a 5/4 increase is always possible.
705 // More is possible if the display is not fully occupied.
706 // Pivot the result around the bottom of the text.
707 final float resultScale = (float)Math.min(1.25f / mResult.getOccupancy(), 2.0);
708 // Keep the right end of text fixed as we scale.
709 mResult.setPivotX(mResult.getWidth() - mResult.getPaddingRight());
710 // Move result up to take place of formula. Scale around top of formula.
711 mResult.setPivotY(mResult.getPaddingTop());
712 float resultTranslationY = -mFormulaText.getHeight();
713 // Move formula off screen.
Hans Boehm08e8f322015-04-21 13:18:38 -0700714 final float formulaTranslationY = -mFormulaText.getBottom();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700715
Hans Boehm84614952014-11-25 18:46:17 -0800716 // TODO: Reintroduce textColorAnimator?
717 // The initial and final colors seemed to be the same in L.
718 // With the new model, the result logically changes back to a formula
719 // only when we switch back to INPUT state, so it's unclear that animating
720 // a color change here makes sense.
721 if (animate) {
722 final AnimatorSet animatorSet = new AnimatorSet();
723 animatorSet.playTogether(
724 ObjectAnimator.ofFloat(mResult, View.SCALE_X, resultScale),
725 ObjectAnimator.ofFloat(mResult, View.SCALE_Y, resultScale),
Hans Boehm84614952014-11-25 18:46:17 -0800726 ObjectAnimator.ofFloat(mResult, View.TRANSLATION_Y, resultTranslationY),
Hans Boehm08e8f322015-04-21 13:18:38 -0700727 ObjectAnimator.ofFloat(mFormulaText, View.TRANSLATION_Y,
Hans Boehm84614952014-11-25 18:46:17 -0800728 formulaTranslationY));
729 animatorSet.setDuration(
730 getResources().getInteger(android.R.integer.config_longAnimTime));
731 animatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
732 animatorSet.addListener(new AnimatorListenerAdapter() {
733 @Override
734 public void onAnimationStart(Animator animation) {
735 // Result should already be displayed; no need to do anything.
736 }
Justin Klaassen4b3af052014-05-27 17:53:10 -0700737
Hans Boehm84614952014-11-25 18:46:17 -0800738 @Override
739 public void onAnimationEnd(Animator animation) {
740 setState(CalculatorState.RESULT);
741 mCurrentAnimator = null;
742 }
743 });
Justin Klaassen4b3af052014-05-27 17:53:10 -0700744
Hans Boehm84614952014-11-25 18:46:17 -0800745 mCurrentAnimator = animatorSet;
746 animatorSet.start();
747 } else /* No animation desired; get there fast, e.g. when restarting */ {
748 mResult.setScaleX(resultScale);
749 mResult.setScaleY(resultScale);
Hans Boehm84614952014-11-25 18:46:17 -0800750 mResult.setTranslationY(resultTranslationY);
Hans Boehm08e8f322015-04-21 13:18:38 -0700751 mFormulaText.setTranslationY(formulaTranslationY);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700752 setState(CalculatorState.RESULT);
Hans Boehm84614952014-11-25 18:46:17 -0800753 }
Justin Klaassen4b3af052014-05-27 17:53:10 -0700754 }
Hans Boehm84614952014-11-25 18:46:17 -0800755
756 // Restore positions of the formula and result displays back to their original,
757 // pre-animation state.
758 private void restoreDisplayPositions() {
759 // Clear result.
760 mResult.setText("");
761 // Reset all of the values modified during the animation.
762 mResult.setScaleX(1.0f);
763 mResult.setScaleY(1.0f);
764 mResult.setTranslationX(0.0f);
765 mResult.setTranslationY(0.0f);
Hans Boehm08e8f322015-04-21 13:18:38 -0700766 mFormulaText.setTranslationY(0.0f);
Hans Boehm84614952014-11-25 18:46:17 -0800767
Hans Boehm08e8f322015-04-21 13:18:38 -0700768 mFormulaText.requestFocus();
Hans Boehm84614952014-11-25 18:46:17 -0800769 }
770
Justin Klaassend48b7562015-04-16 16:51:38 -0700771 @Override
772 public boolean onCreateOptionsMenu(Menu menu) {
Justin Klaassend36d63e2015-05-05 12:59:36 -0700773 super.onCreateOptionsMenu(menu);
774
775 getMenuInflater().inflate(R.menu.activity_calculator, menu);
Justin Klaassend48b7562015-04-16 16:51:38 -0700776 return true;
777 }
778
779 @Override
780 public boolean onPrepareOptionsMenu(Menu menu) {
Justin Klaassend36d63e2015-05-05 12:59:36 -0700781 super.onPrepareOptionsMenu(menu);
782
783 // Show the leading option when displaying a result.
784 menu.findItem(R.id.menu_leading).setVisible(mCurrentState == CalculatorState.RESULT);
785
786 // Show the fraction option when displaying a rational result.
787 menu.findItem(R.id.menu_fraction).setVisible(mCurrentState == CalculatorState.RESULT
788 && mEvaluator.getRational() != null);
789
Justin Klaassend48b7562015-04-16 16:51:38 -0700790 return true;
Hans Boehm84614952014-11-25 18:46:17 -0800791 }
792
793 @Override
Justin Klaassend48b7562015-04-16 16:51:38 -0700794 public boolean onOptionsItemSelected(MenuItem item) {
Hans Boehm84614952014-11-25 18:46:17 -0800795 switch (item.getItemId()) {
Justin Klaassend36d63e2015-05-05 12:59:36 -0700796 case R.id.menu_leading:
797 displayFull();
Hans Boehm84614952014-11-25 18:46:17 -0800798 return true;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700799 case R.id.menu_fraction:
800 displayFraction();
801 return true;
Justin Klaassend36d63e2015-05-05 12:59:36 -0700802 case R.id.menu_licenses:
803 startActivity(new Intent(this, Licenses.class));
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700804 return true;
Hans Boehm84614952014-11-25 18:46:17 -0800805 default:
806 return super.onOptionsItemSelected(item);
807 }
808 }
809
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700810 private void displayMessage(String s) {
Hans Boehm84614952014-11-25 18:46:17 -0800811 AlertDialog.Builder builder = new AlertDialog.Builder(this);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700812 builder.setMessage(s)
813 .setNegativeButton(R.string.dismiss,
Hans Boehm84614952014-11-25 18:46:17 -0800814 new DialogInterface.OnClickListener() {
815 public void onClick(DialogInterface d, int which) { }
816 })
817 .show();
818 }
819
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700820 private void displayFraction() {
821 BoundedRational result = mEvaluator.getRational();
Hans Boehm013969e2015-04-13 20:29:47 -0700822 displayMessage(KeyMaps.translateResult(result.toNiceString()));
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700823 }
824
825 // Display full result to currently evaluated precision
826 private void displayFull() {
827 Resources res = getResources();
828 String msg = mResult.getFullText() + " ";
829 if (mResult.fullTextIsExact()) {
830 msg += res.getString(R.string.exact);
831 } else {
832 msg += res.getString(R.string.approximate);
833 }
834 displayMessage(msg);
835 }
836
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700837 // Add input characters to the end of the expression by mapping them to
838 // the appropriate button pushes when possible. Leftover characters
839 // are added to mUnprocessedChars, which is presumed to immediately
840 // precede the newly added characters.
841 private void addChars(String moreChars) {
842 if (mUnprocessedChars != null) {
843 moreChars = mUnprocessedChars + moreChars;
844 }
845 int current = 0;
846 int len = moreChars.length();
847 while (current < len) {
848 char c = moreChars.charAt(current);
Hans Boehm013969e2015-04-13 20:29:47 -0700849 int k = KeyMaps.keyForChar(c);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700850 if (k != View.NO_ID) {
851 mCurrentButton = findViewById(k);
852 addKeyToExpr(k);
853 if (Character.isSurrogate(c)) {
854 current += 2;
855 } else {
856 ++current;
857 }
858 continue;
859 }
Hans Boehm013969e2015-04-13 20:29:47 -0700860 int f = KeyMaps.funForString(moreChars, current);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700861 if (f != View.NO_ID) {
862 mCurrentButton = findViewById(f);
863 addKeyToExpr(f);
864 if (f == R.id.op_sqrt) {
865 // Square root entered as function; don't lose the parenthesis.
866 addKeyToExpr(R.id.lparen);
867 }
868 current = moreChars.indexOf('(', current) + 1;
869 continue;
870 }
871 // There are characters left, but we can't convert them to button presses.
872 mUnprocessedChars = moreChars.substring(current);
873 redisplayAfterFormulaChange();
874 return;
875 }
876 mUnprocessedChars = null;
877 redisplayAfterFormulaChange();
878 return;
Hans Boehm84614952014-11-25 18:46:17 -0800879 }
880
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700881 @Override
882 public boolean paste(Uri uri) {
883 if (mEvaluator.isLastSaved(uri)) {
884 if (mCurrentState == CalculatorState.ERROR
885 || mCurrentState == CalculatorState.RESULT) {
886 setState(CalculatorState.INPUT);
887 mEvaluator.clear();
Hans Boehm84614952014-11-25 18:46:17 -0800888 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700889 mEvaluator.addSaved();
890 redisplayAfterFormulaChange();
891 return true;
Hans Boehm84614952014-11-25 18:46:17 -0800892 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700893 return false;
Hans Boehm84614952014-11-25 18:46:17 -0800894 }
895
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700896 @Override
897 public void paste(String s) {
898 addChars(s);
Hans Boehm84614952014-11-25 18:46:17 -0800899 }
900
Justin Klaassen4b3af052014-05-27 17:53:10 -0700901}