blob: 81ab1f6511c4e65cd7c6576fa7b6fe4a0e48ffc5 [file] [log] [blame]
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// TODO: Copy & more general paste in formula? Note that this requires
// great care: Currently the text version of a displayed formula
// is not directly useful for re-evaluating the formula later, since
// it contains ellipses representing subexpressions evaluated with
// a different degree mode. Rather than supporting copy from the
// formula window, we may eventually want to support generation of a
// more useful text version in a separate window. It's not clear
// this is worth the added (code and user) complexity.
package com.android.calculator2;
import android.animation.Animator;
import android.animation.Animator.AnimatorListener;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
import android.app.ActionBar;
import android.app.Activity;
import android.app.Fragment;
import android.app.FragmentManager;
import android.app.FragmentTransaction;
import android.content.ClipData;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.Color;
import android.graphics.Rect;
import android.net.Uri;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import androidx.core.content.ContextCompat;
import androidx.viewpager.widget.ViewPager;
import android.text.Editable;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.text.style.ForegroundColorSpan;
import android.util.Log;
import android.util.Property;
import android.view.ActionMode;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnLongClickListener;
import android.view.ViewAnimationUtils;
import android.view.ViewGroupOverlay;
import android.view.ViewTreeObserver;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.widget.HorizontalScrollView;
import android.widget.TextView;
import android.widget.Toolbar;
import com.android.calculator2.CalculatorFormula.OnTextSizeChangeListener;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
import java.text.DecimalFormatSymbols;
import static com.android.calculator2.CalculatorFormula.OnFormulaContextMenuClickListener;
public class Calculator extends Activity
implements OnTextSizeChangeListener, OnLongClickListener,
AlertDialogFragment.OnClickListener, Evaluator.EvaluationListener /* for main result */,
DragLayout.CloseCallback, DragLayout.DragCallback {
private static final String TAG = "Calculator";
/**
* Constant for an invalid resource id.
*/
public static final int INVALID_RES_ID = -1;
private enum CalculatorState {
INPUT, // Result and formula both visible, no evaluation requested,
// Though result may be visible on bottom line.
EVALUATE, // Both visible, evaluation requested, evaluation/animation incomplete.
// Not used for instant result evaluation.
INIT, // Very temporary state used as alternative to EVALUATE
// during reinitialization. Do not animate on completion.
INIT_FOR_RESULT, // Identical to INIT, but evaluation is known to terminate
// with result, and current expression has been copied to history.
ANIMATE, // Result computed, animation to enlarge result window in progress.
RESULT, // Result displayed, formula invisible.
// If we are in RESULT state, the formula was evaluated without
// error to initial precision.
// The current formula is now also the last history entry.
ERROR // Error displayed: Formula visible, result shows error message.
// Display similar to INPUT state.
}
// Normal transition sequence is
// INPUT -> EVALUATE -> ANIMATE -> RESULT (or ERROR) -> INPUT
// A RESULT -> ERROR transition is possible in rare corner cases, in which
// a higher precision evaluation exposes an error. This is possible, since we
// initially evaluate assuming we were given a well-defined problem. If we
// were actually asked to compute sqrt(<extremely tiny negative number>) we produce 0
// unless we are asked for enough precision that we can distinguish the argument from zero.
// ERROR and RESULT are translated to INIT or INIT_FOR_RESULT state if the application
// is restarted in that state. This leads us to recompute and redisplay the result
// ASAP. We avoid saving the ANIMATE state or activating history in that state.
// In INIT_FOR_RESULT, and RESULT state, a copy of the current
// expression has been saved in the history db; in the other non-ANIMATE states,
// it has not.
// TODO: Possibly save a bit more information, e.g. its initial display string
// or most significant digit position, to speed up restart.
private final Property<TextView, Integer> TEXT_COLOR =
new Property<TextView, Integer>(Integer.class, "textColor") {
@Override
public Integer get(TextView textView) {
return textView.getCurrentTextColor();
}
@Override
public void set(TextView textView, Integer textColor) {
textView.setTextColor(textColor);
}
};
private static final String NAME = "Calculator";
private static final String KEY_DISPLAY_STATE = NAME + "_display_state";
private static final String KEY_UNPROCESSED_CHARS = NAME + "_unprocessed_chars";
/**
* Associated value is a byte array holding the evaluator state.
*/
private static final String KEY_EVAL_STATE = NAME + "_eval_state";
private static final String KEY_INVERSE_MODE = NAME + "_inverse_mode";
/**
* Associated value is an boolean holding the visibility state of the toolbar.
*/
private static final String KEY_SHOW_TOOLBAR = NAME + "_show_toolbar";
private final ViewTreeObserver.OnPreDrawListener mPreDrawListener =
new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
mFormulaContainer.scrollTo(mFormulaText.getRight(), 0);
final ViewTreeObserver observer = mFormulaContainer.getViewTreeObserver();
if (observer.isAlive()) {
observer.removeOnPreDrawListener(this);
}
return false;
}
};
private final Evaluator.Callback mEvaluatorCallback = new Evaluator.Callback() {
@Override
public void onMemoryStateChanged() {
mFormulaText.onMemoryStateChanged();
}
@Override
public void showMessageDialog(@StringRes int title, @StringRes int message,
@StringRes int positiveButtonLabel, String tag) {
AlertDialogFragment.showMessageDialog(Calculator.this, title, message,
positiveButtonLabel, tag);
}
};
private final OnDisplayMemoryOperationsListener mOnDisplayMemoryOperationsListener =
new OnDisplayMemoryOperationsListener() {
@Override
public boolean shouldDisplayMemory() {
return mEvaluator.getMemoryIndex() != 0;
}
};
private final OnFormulaContextMenuClickListener mOnFormulaContextMenuClickListener =
new OnFormulaContextMenuClickListener() {
@Override
public boolean onPaste(ClipData clip) {
final ClipData.Item item = clip.getItemCount() == 0 ? null : clip.getItemAt(0);
if (item == null) {
// nothing to paste, bail early...
return false;
}
// Check if the item is a previously copied result, otherwise paste as raw text.
final Uri uri = item.getUri();
if (uri != null && mEvaluator.isLastSaved(uri)) {
clearIfNotInputState();
mEvaluator.appendExpr(mEvaluator.getSavedIndex());
redisplayAfterFormulaChange();
} else {
addChars(item.coerceToText(Calculator.this).toString(), false);
}
return true;
}
@Override
public void onMemoryRecall() {
clearIfNotInputState();
long memoryIndex = mEvaluator.getMemoryIndex();
if (memoryIndex != 0) {
mEvaluator.appendExpr(mEvaluator.getMemoryIndex());
redisplayAfterFormulaChange();
}
}
};
private final TextWatcher mFormulaTextWatcher = new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence charSequence, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence charSequence, int start, int count, int after) {
}
@Override
public void afterTextChanged(Editable editable) {
final ViewTreeObserver observer = mFormulaContainer.getViewTreeObserver();
if (observer.isAlive()) {
observer.removeOnPreDrawListener(mPreDrawListener);
observer.addOnPreDrawListener(mPreDrawListener);
}
}
};
private CalculatorState mCurrentState;
private Evaluator mEvaluator;
private CalculatorDisplay mDisplayView;
private TextView mModeView;
private CalculatorFormula mFormulaText;
private CalculatorResult mResultText;
private HorizontalScrollView mFormulaContainer;
private DragLayout mDragLayout;
private ViewPager mPadViewPager;
private View mDeleteButton;
private View mClearButton;
private View mEqualButton;
private View mMainCalculator;
private TextView mInverseToggle;
private TextView mModeToggle;
private View[] mInvertibleButtons;
private View[] mInverseButtons;
private View mCurrentButton;
private Animator mCurrentAnimator;
// Characters that were recently entered at the end of the display that have not yet
// been added to the underlying expression.
private String mUnprocessedChars = null;
// Color to highlight unprocessed characters from physical keyboard.
// TODO: should probably match this to the error color?
private ForegroundColorSpan mUnprocessedColorSpan = new ForegroundColorSpan(Color.RED);
// Whether the display is one line.
private boolean mIsOneLine;
/**
* Map the old saved state to a new state reflecting requested result reevaluation.
*/
private CalculatorState mapFromSaved(CalculatorState savedState) {
switch (savedState) {
case RESULT:
case INIT_FOR_RESULT:
// Evaluation is expected to terminate normally.
return CalculatorState.INIT_FOR_RESULT;
case ERROR:
case INIT:
return CalculatorState.INIT;
case EVALUATE:
case INPUT:
return savedState;
default: // Includes ANIMATE state.
throw new AssertionError("Impossible saved state");
}
}
/**
* Restore Evaluator state and mCurrentState from savedInstanceState.
* Return true if the toolbar should be visible.
*/
private void restoreInstanceState(Bundle savedInstanceState) {
final CalculatorState savedState = CalculatorState.values()[
savedInstanceState.getInt(KEY_DISPLAY_STATE,
CalculatorState.INPUT.ordinal())];
setState(savedState);
CharSequence unprocessed = savedInstanceState.getCharSequence(KEY_UNPROCESSED_CHARS);
if (unprocessed != null) {
mUnprocessedChars = unprocessed.toString();
}
byte[] state = savedInstanceState.getByteArray(KEY_EVAL_STATE);
if (state != null) {
try (ObjectInput in = new ObjectInputStream(new ByteArrayInputStream(state))) {
mEvaluator.restoreInstanceState(in);
} catch (Throwable ignored) {
// When in doubt, revert to clean state
mCurrentState = CalculatorState.INPUT;
mEvaluator.clearMain();
}
}
if (savedInstanceState.getBoolean(KEY_SHOW_TOOLBAR, true)) {
showAndMaybeHideToolbar();
} else {
mDisplayView.hideToolbar();
}
onInverseToggled(savedInstanceState.getBoolean(KEY_INVERSE_MODE));
// TODO: We're currently not saving and restoring scroll position.
// We probably should. Details may require care to deal with:
// - new display size
// - slow recomputation if we've scrolled far.
}
private void restoreDisplay() {
onModeChanged(mEvaluator.getDegreeMode(Evaluator.MAIN_INDEX));
if (mCurrentState != CalculatorState.RESULT
&& mCurrentState != CalculatorState.INIT_FOR_RESULT) {
redisplayFormula();
}
if (mCurrentState == CalculatorState.INPUT) {
// This resultText will explicitly call evaluateAndNotify when ready.
mResultText.setShouldEvaluateResult(CalculatorResult.SHOULD_EVALUATE, this);
} else {
// Just reevaluate.
setState(mapFromSaved(mCurrentState));
// Request evaluation when we know display width.
mResultText.setShouldEvaluateResult(CalculatorResult.SHOULD_REQUIRE, this);
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_calculator_main);
setActionBar((Toolbar) findViewById(R.id.toolbar));
// Hide all default options in the ActionBar.
getActionBar().setDisplayOptions(0);
// Ensure the toolbar stays visible while the options menu is displayed.
getActionBar().addOnMenuVisibilityListener(new ActionBar.OnMenuVisibilityListener() {
@Override
public void onMenuVisibilityChanged(boolean isVisible) {
mDisplayView.setForceToolbarVisible(isVisible);
}
});
mMainCalculator = findViewById(R.id.main_calculator);
mDisplayView = (CalculatorDisplay) findViewById(R.id.display);
mModeView = (TextView) findViewById(R.id.mode);
mFormulaText = (CalculatorFormula) findViewById(R.id.formula);
mResultText = (CalculatorResult) findViewById(R.id.result);
mFormulaContainer = (HorizontalScrollView) findViewById(R.id.formula_container);
mEvaluator = Evaluator.getInstance(this);
mEvaluator.setCallback(mEvaluatorCallback);
mResultText.setEvaluator(mEvaluator, Evaluator.MAIN_INDEX);
KeyMaps.setActivity(this);
mPadViewPager = (ViewPager) findViewById(R.id.pad_pager);
mDeleteButton = findViewById(R.id.del);
mClearButton = findViewById(R.id.clr);
final View numberPad = findViewById(R.id.pad_numeric);
mEqualButton = numberPad.findViewById(R.id.eq);
if (mEqualButton == null || mEqualButton.getVisibility() != View.VISIBLE) {
mEqualButton = findViewById(R.id.pad_operator).findViewById(R.id.eq);
}
final TextView decimalPointButton = (TextView) numberPad.findViewById(R.id.dec_point);
decimalPointButton.setText(getDecimalSeparator());
mInverseToggle = (TextView) findViewById(R.id.toggle_inv);
mModeToggle = (TextView) findViewById(R.id.toggle_mode);
mIsOneLine = mResultText.getVisibility() == View.INVISIBLE;
mInvertibleButtons = new View[] {
findViewById(R.id.fun_sin),
findViewById(R.id.fun_cos),
findViewById(R.id.fun_tan),
findViewById(R.id.fun_ln),
findViewById(R.id.fun_log),
findViewById(R.id.op_sqrt)
};
mInverseButtons = new View[] {
findViewById(R.id.fun_arcsin),
findViewById(R.id.fun_arccos),
findViewById(R.id.fun_arctan),
findViewById(R.id.fun_exp),
findViewById(R.id.fun_10pow),
findViewById(R.id.op_sqr)
};
mDragLayout = (DragLayout) findViewById(R.id.drag_layout);
mDragLayout.removeDragCallback(this);
mDragLayout.addDragCallback(this);
mDragLayout.setCloseCallback(this);
mFormulaText.setOnContextMenuClickListener(mOnFormulaContextMenuClickListener);
mFormulaText.setOnDisplayMemoryOperationsListener(mOnDisplayMemoryOperationsListener);
mFormulaText.setOnTextSizeChangeListener(this);
mFormulaText.addTextChangedListener(mFormulaTextWatcher);
mDeleteButton.setOnLongClickListener(this);
if (savedInstanceState != null) {
restoreInstanceState(savedInstanceState);
} else {
mCurrentState = CalculatorState.INPUT;
mEvaluator.clearMain();
showAndMaybeHideToolbar();
onInverseToggled(false);
}
restoreDisplay();
}
@Override
protected void onResume() {
super.onResume();
if (mDisplayView.isToolbarVisible()) {
showAndMaybeHideToolbar();
}
// If HistoryFragment is showing, hide the main Calculator elements from accessibility.
// This is because Talkback does not use visibility as a cue for RelativeLayout elements,
// and RelativeLayout is the base class of DragLayout.
// If we did not do this, it would be possible to traverse to main Calculator elements from
// HistoryFragment.
mMainCalculator.setImportantForAccessibility(
mDragLayout.isOpen() ? View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
: View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
}
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
mEvaluator.cancelAll(true);
// If there's an animation in progress, cancel it first to ensure our state is up-to-date.
if (mCurrentAnimator != null) {
mCurrentAnimator.cancel();
}
super.onSaveInstanceState(outState);
outState.putInt(KEY_DISPLAY_STATE, mCurrentState.ordinal());
outState.putCharSequence(KEY_UNPROCESSED_CHARS, mUnprocessedChars);
ByteArrayOutputStream byteArrayStream = new ByteArrayOutputStream();
try (ObjectOutput out = new ObjectOutputStream(byteArrayStream)) {
mEvaluator.saveInstanceState(out);
} catch (IOException e) {
// Impossible; No IO involved.
throw new AssertionError("Impossible IO exception", e);
}
outState.putByteArray(KEY_EVAL_STATE, byteArrayStream.toByteArray());
outState.putBoolean(KEY_INVERSE_MODE, mInverseToggle.isSelected());
outState.putBoolean(KEY_SHOW_TOOLBAR, mDisplayView.isToolbarVisible());
// We must wait for asynchronous writes to complete, since outState may contain
// references to expressions being written.
mEvaluator.waitForWrites();
}
// Set the state, updating delete label and display colors.
// This restores display positions on moving to INPUT.
// But movement/animation for moving to RESULT has already been done.
private void setState(CalculatorState state) {
if (mCurrentState != state) {
if (state == CalculatorState.INPUT) {
// We'll explicitly request evaluation from now on.
mResultText.setShouldEvaluateResult(CalculatorResult.SHOULD_NOT_EVALUATE, null);
restoreDisplayPositions();
}
mCurrentState = state;
if (mCurrentState == CalculatorState.RESULT) {
// No longer do this for ERROR; allow mistakes to be corrected.
mDeleteButton.setVisibility(View.GONE);
mClearButton.setVisibility(View.VISIBLE);
} else {
mDeleteButton.setVisibility(View.VISIBLE);
mClearButton.setVisibility(View.GONE);
}
if (mIsOneLine) {
if (mCurrentState == CalculatorState.RESULT
|| mCurrentState == CalculatorState.EVALUATE
|| mCurrentState == CalculatorState.ANIMATE) {
mFormulaText.setVisibility(View.VISIBLE);
mResultText.setVisibility(View.VISIBLE);
} else if (mCurrentState == CalculatorState.ERROR) {
mFormulaText.setVisibility(View.INVISIBLE);
mResultText.setVisibility(View.VISIBLE);
} else {
mFormulaText.setVisibility(View.VISIBLE);
mResultText.setVisibility(View.INVISIBLE);
}
}
if (mCurrentState == CalculatorState.ERROR) {
final int errorColor =
ContextCompat.getColor(this, R.color.calculator_error_color);
mFormulaText.setTextColor(errorColor);
mResultText.setTextColor(errorColor);
getWindow().setStatusBarColor(errorColor);
} else if (mCurrentState != CalculatorState.RESULT) {
mFormulaText.setTextColor(
ContextCompat.getColor(this, R.color.display_formula_text_color));
mResultText.setTextColor(
ContextCompat.getColor(this, R.color.display_result_text_color));
getWindow().setStatusBarColor(
ContextCompat.getColor(this, R.color.calculator_statusbar_color));
}
invalidateOptionsMenu();
}
}
public boolean isResultLayout() {
// Note that ERROR has INPUT, not RESULT layout.
return mCurrentState == CalculatorState.INIT_FOR_RESULT
|| mCurrentState == CalculatorState.RESULT;
}
public boolean isOneLine() {
return mIsOneLine;
}
@Override
protected void onDestroy() {
mDragLayout.removeDragCallback(this);
super.onDestroy();
}
/**
* Destroy the evaluator and close the underlying database.
*/
public void destroyEvaluator() {
mEvaluator.destroyEvaluator();
}
@Override
public void onActionModeStarted(ActionMode mode) {
super.onActionModeStarted(mode);
if (mode.getTag() == CalculatorFormula.TAG_ACTION_MODE) {
mFormulaContainer.scrollTo(mFormulaText.getRight(), 0);
}
}
/**
* Stop any active ActionMode or ContextMenu for copy/paste actions.
* Return true if there was one.
*/
private boolean stopActionModeOrContextMenu() {
return mResultText.stopActionModeOrContextMenu()
|| mFormulaText.stopActionModeOrContextMenu();
}
@Override
public void onUserInteraction() {
super.onUserInteraction();
// If there's an animation in progress, end it immediately, so the user interaction can
// be handled.
if (mCurrentAnimator != null) {
mCurrentAnimator.end();
}
}
@Override
public boolean dispatchTouchEvent(MotionEvent e) {
if (e.getActionMasked() == MotionEvent.ACTION_DOWN) {
stopActionModeOrContextMenu();
final HistoryFragment historyFragment = getHistoryFragment();
if (mDragLayout.isOpen() && historyFragment != null) {
historyFragment.stopActionModeOrContextMenu();
}
}
return super.dispatchTouchEvent(e);
}
@Override
public void onBackPressed() {
if (!stopActionModeOrContextMenu()) {
final HistoryFragment historyFragment = getHistoryFragment();
if (mDragLayout.isOpen() && historyFragment != null) {
if (!historyFragment.stopActionModeOrContextMenu()) {
removeHistoryFragment();
}
return;
}
if (mPadViewPager != null && mPadViewPager.getCurrentItem() != 0) {
// Select the previous pad.
mPadViewPager.setCurrentItem(mPadViewPager.getCurrentItem() - 1);
} else {
// If the user is currently looking at the first pad (or the pad is not paged),
// allow the system to handle the Back button.
super.onBackPressed();
}
}
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
// Allow the system to handle special key codes (e.g. "BACK" or "DPAD").
switch (keyCode) {
case KeyEvent.KEYCODE_BACK:
case KeyEvent.KEYCODE_ESCAPE:
case KeyEvent.KEYCODE_DPAD_UP:
case KeyEvent.KEYCODE_DPAD_DOWN:
case KeyEvent.KEYCODE_DPAD_LEFT:
case KeyEvent.KEYCODE_DPAD_RIGHT:
return super.onKeyUp(keyCode, event);
}
// Stop the action mode or context menu if it's showing.
stopActionModeOrContextMenu();
// Always cancel unrequested in-progress evaluation of the main expression, so that
// we don't have to worry about subsequent asynchronous completion.
// Requested in-progress evaluations are handled below.
cancelUnrequested();
switch (keyCode) {
case KeyEvent.KEYCODE_NUMPAD_ENTER:
case KeyEvent.KEYCODE_ENTER:
case KeyEvent.KEYCODE_DPAD_CENTER:
mCurrentButton = mEqualButton;
onEquals();
return true;
case KeyEvent.KEYCODE_DEL:
mCurrentButton = mDeleteButton;
onDelete();
return true;
case KeyEvent.KEYCODE_CLEAR:
mCurrentButton = mClearButton;
onClear();
return true;
default:
cancelIfEvaluating(false);
final int raw = event.getKeyCharacterMap().get(keyCode, event.getMetaState());
if ((raw & KeyCharacterMap.COMBINING_ACCENT) != 0) {
return true; // discard
}
// Try to discard non-printing characters and the like.
// The user will have to explicitly delete other junk that gets past us.
if (Character.isIdentifierIgnorable(raw) || Character.isWhitespace(raw)) {
return true;
}
char c = (char) raw;
if (c == '=') {
mCurrentButton = mEqualButton;
onEquals();
} else {
addChars(String.valueOf(c), true);
redisplayAfterFormulaChange();
}
return true;
}
}
/**
* Invoked whenever the inverse button is toggled to update the UI.
*
* @param showInverse {@code true} if inverse functions should be shown
*/
private void onInverseToggled(boolean showInverse) {
mInverseToggle.setSelected(showInverse);
if (showInverse) {
mInverseToggle.setContentDescription(getString(R.string.desc_inv_on));
for (View invertibleButton : mInvertibleButtons) {
invertibleButton.setVisibility(View.GONE);
}
for (View inverseButton : mInverseButtons) {
inverseButton.setVisibility(View.VISIBLE);
}
} else {
mInverseToggle.setContentDescription(getString(R.string.desc_inv_off));
for (View invertibleButton : mInvertibleButtons) {
invertibleButton.setVisibility(View.VISIBLE);
}
for (View inverseButton : mInverseButtons) {
inverseButton.setVisibility(View.GONE);
}
}
}
/**
* Invoked whenever the deg/rad mode may have changed to update the UI. Note that the mode has
* not necessarily actually changed where this is invoked.
*
* @param degreeMode {@code true} if in degree mode
*/
private void onModeChanged(boolean degreeMode) {
if (degreeMode) {
mModeView.setText(R.string.mode_deg);
mModeView.setContentDescription(getString(R.string.desc_mode_deg));
mModeToggle.setText(R.string.mode_rad);
mModeToggle.setContentDescription(getString(R.string.desc_switch_rad));
} else {
mModeView.setText(R.string.mode_rad);
mModeView.setContentDescription(getString(R.string.desc_mode_rad));
mModeToggle.setText(R.string.mode_deg);
mModeToggle.setContentDescription(getString(R.string.desc_switch_deg));
}
}
private void removeHistoryFragment() {
final FragmentManager manager = getFragmentManager();
if (manager != null && !manager.isDestroyed()) {
manager.popBackStack(HistoryFragment.TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE);
}
// When HistoryFragment is hidden, the main Calculator is important for accessibility again.
mMainCalculator.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
}
/**
* Switch to INPUT from RESULT state in response to input of the specified button_id.
* View.NO_ID is treated as an incomplete function id.
*/
private void switchToInput(int button_id) {
if (KeyMaps.isBinary(button_id) || KeyMaps.isSuffix(button_id)) {
mEvaluator.collapse(mEvaluator.getMaxIndex() /* Most recent history entry */);
} else {
announceClearedForAccessibility();
mEvaluator.clearMain();
}
setState(CalculatorState.INPUT);
}
// Add the given button id to input expression.
// If appropriate, clear the expression before doing so.
private void addKeyToExpr(int id) {
if (mCurrentState == CalculatorState.ERROR) {
setState(CalculatorState.INPUT);
} else if (mCurrentState == CalculatorState.RESULT) {
switchToInput(id);
}
if (!mEvaluator.append(id)) {
// TODO: Some user visible feedback?
}
}
/**
* Add the given button id to input expression, assuming it was explicitly
* typed/touched.
* We perform slightly more aggressive correction than in pasted expressions.
*/
private void addExplicitKeyToExpr(int id) {
if (mCurrentState == CalculatorState.INPUT && id == R.id.op_sub) {
mEvaluator.getExpr(Evaluator.MAIN_INDEX).removeTrailingAdditiveOperators();
}
addKeyToExpr(id);
}
public void evaluateInstantIfNecessary() {
if (mCurrentState == CalculatorState.INPUT
&& mEvaluator.getExpr(Evaluator.MAIN_INDEX).hasInterestingOps()) {
mEvaluator.evaluateAndNotify(Evaluator.MAIN_INDEX, this, mResultText);
}
}
private void redisplayAfterFormulaChange() {
// TODO: Could do this more incrementally.
redisplayFormula();
setState(CalculatorState.INPUT);
mResultText.clear();
if (haveUnprocessed()) {
// Force reevaluation when text is deleted, even if expression is unchanged.
mEvaluator.touch();
} else {
evaluateInstantIfNecessary();
}
}
/**
* Show the toolbar.
* Automatically hide it again if it's not relevant to current formula.
*/
private void showAndMaybeHideToolbar() {
final boolean shouldBeVisible =
mCurrentState == CalculatorState.INPUT && mEvaluator.hasTrigFuncs();
mDisplayView.showToolbar(!shouldBeVisible);
}
/**
* Display or hide the toolbar depending on calculator state.
*/
private void showOrHideToolbar() {
final boolean shouldBeVisible =
mCurrentState == CalculatorState.INPUT && mEvaluator.hasTrigFuncs();
if (shouldBeVisible) {
mDisplayView.showToolbar(false);
} else {
mDisplayView.hideToolbar();
}
}
public void onButtonClick(View view) {
// Any animation is ended before we get here.
mCurrentButton = view;
stopActionModeOrContextMenu();
// See onKey above for the rationale behind some of the behavior below:
cancelUnrequested();
final int id = view.getId();
switch (id) {
case R.id.eq:
onEquals();
break;
case R.id.del:
onDelete();
break;
case R.id.clr:
onClear();
return; // Toolbar visibility adjusted at end of animation.
case R.id.toggle_inv:
final boolean selected = !mInverseToggle.isSelected();
mInverseToggle.setSelected(selected);
onInverseToggled(selected);
if (mCurrentState == CalculatorState.RESULT) {
mResultText.redisplay(); // In case we cancelled reevaluation.
}
break;
case R.id.toggle_mode:
cancelIfEvaluating(false);
final boolean mode = !mEvaluator.getDegreeMode(Evaluator.MAIN_INDEX);
if (mCurrentState == CalculatorState.RESULT
&& mEvaluator.getExpr(Evaluator.MAIN_INDEX).hasTrigFuncs()) {
// Capture current result evaluated in old mode.
mEvaluator.collapse(mEvaluator.getMaxIndex());
redisplayFormula();
}
// In input mode, we reinterpret already entered trig functions.
mEvaluator.setDegreeMode(mode);
onModeChanged(mode);
// Show the toolbar to highlight the mode change.
showAndMaybeHideToolbar();
setState(CalculatorState.INPUT);
mResultText.clear();
if (!haveUnprocessed()) {
evaluateInstantIfNecessary();
}
return;
default:
cancelIfEvaluating(false);
if (haveUnprocessed()) {
// For consistency, append as uninterpreted characters.
// This may actually be useful for a left parenthesis.
addChars(KeyMaps.toString(this, id), true);
} else {
addExplicitKeyToExpr(id);
redisplayAfterFormulaChange();
}
break;
}
showOrHideToolbar();
}
void redisplayFormula() {
SpannableStringBuilder formula
= mEvaluator.getExpr(Evaluator.MAIN_INDEX).toSpannableStringBuilder(this);
if (mUnprocessedChars != null) {
// Add and highlight characters we couldn't process.
formula.append(mUnprocessedChars, mUnprocessedColorSpan,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
mFormulaText.changeTextTo(formula);
mFormulaText.setContentDescription(TextUtils.isEmpty(formula)
? getString(R.string.desc_formula) : null);
}
@Override
public boolean onLongClick(View view) {
mCurrentButton = view;
if (view.getId() == R.id.del) {
onClear();
return true;
}
return false;
}
// Initial evaluation completed successfully. Initiate display.
public void onEvaluate(long index, int initDisplayPrec, int msd, int leastDigPos,
String truncatedWholeNumber) {
if (index != Evaluator.MAIN_INDEX) {
throw new AssertionError("Unexpected evaluation result index\n");
}
// Invalidate any options that may depend on the current result.
invalidateOptionsMenu();
mResultText.onEvaluate(index, initDisplayPrec, msd, leastDigPos, truncatedWholeNumber);
if (mCurrentState != CalculatorState.INPUT) {
// In EVALUATE, INIT, RESULT, or INIT_FOR_RESULT state.
onResult(mCurrentState == CalculatorState.EVALUATE /* animate */,
mCurrentState == CalculatorState.INIT_FOR_RESULT
|| mCurrentState == CalculatorState.RESULT /* previously preserved */);
}
}
// Reset state to reflect evaluator cancellation. Invoked by evaluator.
public void onCancelled(long index) {
// Index is Evaluator.MAIN_INDEX. We should be in EVALUATE state.
setState(CalculatorState.INPUT);
mResultText.onCancelled(index);
}
// Reevaluation completed; ask result to redisplay current value.
public void onReevaluate(long index) {
// Index is Evaluator.MAIN_INDEX.
mResultText.onReevaluate(index);
}
@Override
public void onTextSizeChanged(final TextView textView, float oldSize) {
if (mCurrentState != CalculatorState.INPUT) {
// Only animate text changes that occur from user input.
return;
}
// Calculate the values needed to perform the scale and translation animations,
// maintaining the same apparent baseline for the displayed text.
final float textScale = oldSize / textView.getTextSize();
final float translationX = (1.0f - textScale) *
(textView.getWidth() / 2.0f - textView.getPaddingEnd());
final float translationY = (1.0f - textScale) *
(textView.getHeight() / 2.0f - textView.getPaddingBottom());
final AnimatorSet animatorSet = new AnimatorSet();
animatorSet.playTogether(
ObjectAnimator.ofFloat(textView, View.SCALE_X, textScale, 1.0f),
ObjectAnimator.ofFloat(textView, View.SCALE_Y, textScale, 1.0f),
ObjectAnimator.ofFloat(textView, View.TRANSLATION_X, translationX, 0.0f),
ObjectAnimator.ofFloat(textView, View.TRANSLATION_Y, translationY, 0.0f));
animatorSet.setDuration(getResources().getInteger(android.R.integer.config_mediumAnimTime));
animatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
animatorSet.start();
}
/**
* Cancel any in-progress explicitly requested evaluations.
* @param quiet suppress pop-up message. Explicit evaluation can change the expression
value, and certainly changes the display, so it seems reasonable to warn.
* @return true if there was such an evaluation
*/
private boolean cancelIfEvaluating(boolean quiet) {
if (mCurrentState == CalculatorState.EVALUATE) {
mEvaluator.cancel(Evaluator.MAIN_INDEX, quiet);
return true;
} else {
return false;
}
}
private void cancelUnrequested() {
if (mCurrentState == CalculatorState.INPUT) {
mEvaluator.cancel(Evaluator.MAIN_INDEX, true);
}
}
private boolean haveUnprocessed() {
return mUnprocessedChars != null && !mUnprocessedChars.isEmpty();
}
private void onEquals() {
// Ignore if in non-INPUT state, or if there are no operators.
if (mCurrentState == CalculatorState.INPUT) {
if (haveUnprocessed()) {
setState(CalculatorState.EVALUATE);
onError(Evaluator.MAIN_INDEX, R.string.error_syntax);
} else if (mEvaluator.getExpr(Evaluator.MAIN_INDEX).hasInterestingOps()) {
setState(CalculatorState.EVALUATE);
mEvaluator.requireResult(Evaluator.MAIN_INDEX, this, mResultText);
}
}
}
private void onDelete() {
// Delete works like backspace; remove the last character or operator from the expression.
// Note that we handle keyboard delete exactly like the delete button. For
// example the delete button can be used to delete a character from an incomplete
// function name typed on a physical keyboard.
// This should be impossible in RESULT state.
// If there is an in-progress explicit evaluation, just cancel it and return.
if (cancelIfEvaluating(false)) return;
setState(CalculatorState.INPUT);
if (haveUnprocessed()) {
mUnprocessedChars = mUnprocessedChars.substring(0, mUnprocessedChars.length() - 1);
} else {
mEvaluator.delete();
}
if (mEvaluator.getExpr(Evaluator.MAIN_INDEX).isEmpty() && !haveUnprocessed()) {
// Resulting formula won't be announced, since it's empty.
announceClearedForAccessibility();
}
redisplayAfterFormulaChange();
}
private void reveal(View sourceView, int colorRes, AnimatorListener listener) {
final ViewGroupOverlay groupOverlay =
(ViewGroupOverlay) getWindow().getDecorView().getOverlay();
final Rect displayRect = new Rect();
mDisplayView.getGlobalVisibleRect(displayRect);
// Make reveal cover the display and status bar.
final View revealView = new View(this);
revealView.setBottom(displayRect.bottom);
revealView.setLeft(displayRect.left);
revealView.setRight(displayRect.right);
revealView.setBackgroundColor(ContextCompat.getColor(this, colorRes));
groupOverlay.add(revealView);
final int[] clearLocation = new int[2];
sourceView.getLocationInWindow(clearLocation);
clearLocation[0] += sourceView.getWidth() / 2;
clearLocation[1] += sourceView.getHeight() / 2;
final int revealCenterX = clearLocation[0] - revealView.getLeft();
final int revealCenterY = clearLocation[1] - revealView.getTop();
final double x1_2 = Math.pow(revealView.getLeft() - revealCenterX, 2);
final double x2_2 = Math.pow(revealView.getRight() - revealCenterX, 2);
final double y_2 = Math.pow(revealView.getTop() - revealCenterY, 2);
final float revealRadius = (float) Math.max(Math.sqrt(x1_2 + y_2), Math.sqrt(x2_2 + y_2));
final Animator revealAnimator =
ViewAnimationUtils.createCircularReveal(revealView,
revealCenterX, revealCenterY, 0.0f, revealRadius);
revealAnimator.setDuration(
getResources().getInteger(android.R.integer.config_longAnimTime));
revealAnimator.addListener(listener);
final Animator alphaAnimator = ObjectAnimator.ofFloat(revealView, View.ALPHA, 0.0f);
alphaAnimator.setDuration(
getResources().getInteger(android.R.integer.config_mediumAnimTime));
final AnimatorSet animatorSet = new AnimatorSet();
animatorSet.play(revealAnimator).before(alphaAnimator);
animatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
animatorSet.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animator) {
groupOverlay.remove(revealView);
mCurrentAnimator = null;
}
});
mCurrentAnimator = animatorSet;
animatorSet.start();
}
private void announceClearedForAccessibility() {
mResultText.announceForAccessibility(getResources().getString(R.string.cleared));
}
public void onClearAnimationEnd() {
mUnprocessedChars = null;
mResultText.clear();
mEvaluator.clearMain();
setState(CalculatorState.INPUT);
redisplayFormula();
}
private void onClear() {
if (mEvaluator.getExpr(Evaluator.MAIN_INDEX).isEmpty() && !haveUnprocessed()) {
return;
}
cancelIfEvaluating(true);
announceClearedForAccessibility();
reveal(mCurrentButton, R.color.calculator_primary_color, new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
onClearAnimationEnd();
showOrHideToolbar();
}
});
}
// Evaluation encountered en error. Display the error.
@Override
public void onError(final long index, final int errorResourceId) {
if (index != Evaluator.MAIN_INDEX) {
throw new AssertionError("Unexpected error source");
}
if (mCurrentState == CalculatorState.EVALUATE) {
setState(CalculatorState.ANIMATE);
mResultText.announceForAccessibility(getResources().getString(errorResourceId));
reveal(mCurrentButton, R.color.calculator_error_color,
new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
setState(CalculatorState.ERROR);
mResultText.onError(index, errorResourceId);
}
});
} else if (mCurrentState == CalculatorState.INIT
|| mCurrentState == CalculatorState.INIT_FOR_RESULT /* very unlikely */) {
setState(CalculatorState.ERROR);
mResultText.onError(index, errorResourceId);
} else {
mResultText.clear();
}
}
// Animate movement of result into the top formula slot.
// Result window now remains translated in the top slot while the result is displayed.
// (We convert it back to formula use only when the user provides new input.)
// Historical note: In the Lollipop version, this invisibly and instantaneously moved
// formula and result displays back at the end of the animation. We no longer do that,
// so that we can continue to properly support scrolling of the result.
// We assume the result already contains the text to be expanded.
private void onResult(boolean animate, boolean resultWasPreserved) {
// Calculate the textSize that would be used to display the result in the formula.
// For scrollable results just use the minimum textSize to maximize the number of digits
// that are visible on screen.
float textSize = mFormulaText.getMinimumTextSize();
if (!mResultText.isScrollable()) {
textSize = mFormulaText.getVariableTextSize(mResultText.getText().toString());
}
// Scale the result to match the calculated textSize, minimizing the jump-cut transition
// when a result is reused in a subsequent expression.
final float resultScale = textSize / mResultText.getTextSize();
// Set the result's pivot to match its gravity.
mResultText.setPivotX(mResultText.getWidth() - mResultText.getPaddingRight());
mResultText.setPivotY(mResultText.getHeight() - mResultText.getPaddingBottom());
// Calculate the necessary translations so the result takes the place of the formula and
// the formula moves off the top of the screen.
final float resultTranslationY = (mFormulaContainer.getBottom() - mResultText.getBottom())
- (mFormulaText.getPaddingBottom() - mResultText.getPaddingBottom());
float formulaTranslationY = -mFormulaContainer.getBottom();
if (mIsOneLine) {
// Position the result text.
mResultText.setY(mResultText.getBottom());
formulaTranslationY = -(findViewById(R.id.toolbar).getBottom()
+ mFormulaContainer.getBottom());
}
// Change the result's textColor to match the formula.
final int formulaTextColor = mFormulaText.getCurrentTextColor();
if (resultWasPreserved) {
// Result was previously addded to history.
mEvaluator.represerve();
} else {
// Add current result to history.
mEvaluator.preserve(Evaluator.MAIN_INDEX, true);
}
if (animate) {
mResultText.announceForAccessibility(getResources().getString(R.string.desc_eq));
mResultText.announceForAccessibility(mResultText.getText());
setState(CalculatorState.ANIMATE);
final AnimatorSet animatorSet = new AnimatorSet();
animatorSet.playTogether(
ObjectAnimator.ofPropertyValuesHolder(mResultText,
PropertyValuesHolder.ofFloat(View.SCALE_X, resultScale),
PropertyValuesHolder.ofFloat(View.SCALE_Y, resultScale),
PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, resultTranslationY)),
ObjectAnimator.ofArgb(mResultText, TEXT_COLOR, formulaTextColor),
ObjectAnimator.ofFloat(mFormulaContainer, View.TRANSLATION_Y,
formulaTranslationY));
animatorSet.setDuration(getResources().getInteger(
android.R.integer.config_longAnimTime));
animatorSet.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
setState(CalculatorState.RESULT);
mCurrentAnimator = null;
}
});
mCurrentAnimator = animatorSet;
animatorSet.start();
} else /* No animation desired; get there fast when restarting */ {
mResultText.setScaleX(resultScale);
mResultText.setScaleY(resultScale);
mResultText.setTranslationY(resultTranslationY);
mResultText.setTextColor(formulaTextColor);
mFormulaContainer.setTranslationY(formulaTranslationY);
setState(CalculatorState.RESULT);
}
}
// Restore positions of the formula and result displays back to their original,
// pre-animation state.
private void restoreDisplayPositions() {
// Clear result.
mResultText.setText("");
// Reset all of the values modified during the animation.
mResultText.setScaleX(1.0f);
mResultText.setScaleY(1.0f);
mResultText.setTranslationX(0.0f);
mResultText.setTranslationY(0.0f);
mFormulaContainer.setTranslationY(0.0f);
mFormulaText.requestFocus();
}
@Override
public void onClick(AlertDialogFragment fragment, int which) {
if (which == DialogInterface.BUTTON_POSITIVE) {
if (HistoryFragment.CLEAR_DIALOG_TAG.equals(fragment.getTag())) {
// TODO: Try to preserve the current, saved, and memory expressions. How should we
// handle expressions to which they refer?
mEvaluator.clearEverything();
// TODO: It's not clear what we should really do here. This is an initial hack.
// May want to make onClearAnimationEnd() private if/when we fix this.
onClearAnimationEnd();
mEvaluatorCallback.onMemoryStateChanged();
onBackPressed();
} else if (Evaluator.TIMEOUT_DIALOG_TAG.equals(fragment.getTag())) {
// Timeout extension request.
mEvaluator.setLongTimeout();
} else {
Log.e(TAG, "Unknown AlertDialogFragment click:" + fragment.getTag());
}
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
getMenuInflater().inflate(R.menu.activity_calculator, menu);
return true;
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu);
// Show the leading option when displaying a result.
menu.findItem(R.id.menu_leading).setVisible(mCurrentState == CalculatorState.RESULT);
// Show the fraction option when displaying a rational result.
boolean visible = mCurrentState == CalculatorState.RESULT;
final UnifiedReal mainResult = mEvaluator.getResult(Evaluator.MAIN_INDEX);
// mainResult should never be null, but it happens. Check as a workaround to protect
// against crashes until we find the root cause (b/34763650).
visible &= mainResult != null && mainResult.exactlyDisplayable();
menu.findItem(R.id.menu_fraction).setVisible(visible);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_history:
showHistoryFragment();
return true;
case R.id.menu_leading:
displayFull();
return true;
case R.id.menu_fraction:
displayFraction();
return true;
case R.id.menu_licenses:
startActivity(new Intent(this, Licenses.class));
return true;
default:
return super.onOptionsItemSelected(item);
}
}
/* Begin override CloseCallback method. */
@Override
public void onClose() {
removeHistoryFragment();
}
/* End override CloseCallback method. */
/* Begin override DragCallback methods */
public void onStartDraggingOpen() {
mDisplayView.hideToolbar();
showHistoryFragment();
}
@Override
public void onInstanceStateRestored(boolean isOpen) {
}
@Override
public void whileDragging(float yFraction) {
}
@Override
public boolean shouldCaptureView(View view, int x, int y) {
return view.getId() == R.id.history_frame
&& (mDragLayout.isMoving() || mDragLayout.isViewUnder(view, x, y));
}
@Override
public int getDisplayHeight() {
return mDisplayView.getMeasuredHeight();
}
/* End override DragCallback methods */
/**
* Change evaluation state to one that's friendly to the history fragment.
* Return false if that was not easily possible.
*/
private boolean prepareForHistory() {
if (mCurrentState == CalculatorState.ANIMATE) {
// End the current animation and signal that preparation has failed.
// onUserInteraction is unreliable and onAnimationEnd() is asynchronous, so we
// aren't guaranteed to be out of the ANIMATE state by the time prepareForHistory is
// called.
if (mCurrentAnimator != null) {
mCurrentAnimator.end();
}
return false;
} else if (mCurrentState == CalculatorState.EVALUATE) {
// Cancel current evaluation
cancelIfEvaluating(true /* quiet */ );
setState(CalculatorState.INPUT);
return true;
} else if (mCurrentState == CalculatorState.INIT) {
// Easiest to just refuse. Otherwise we can see a state change
// while in history mode, which causes all sorts of problems.
// TODO: Consider other alternatives. If we're just doing the decimal conversion
// at the end of an evaluation, we could treat this as RESULT state.
return false;
}
// We should be in INPUT, INIT_FOR_RESULT, RESULT, or ERROR state.
return true;
}
private HistoryFragment getHistoryFragment() {
final FragmentManager manager = getFragmentManager();
if (manager == null || manager.isDestroyed()) {
return null;
}
final Fragment fragment = manager.findFragmentByTag(HistoryFragment.TAG);
return fragment == null || fragment.isRemoving() ? null : (HistoryFragment) fragment;
}
private void showHistoryFragment() {
if (getHistoryFragment() != null) {
// If the fragment already exists, do nothing.
return;
}
final FragmentManager manager = getFragmentManager();
if (manager == null || manager.isDestroyed() || !prepareForHistory()) {
// If the history fragment can not be shown, close the draglayout.
mDragLayout.setClosed();
return;
}
stopActionModeOrContextMenu();
manager.beginTransaction()
.replace(R.id.history_frame, new HistoryFragment(), HistoryFragment.TAG)
.setTransition(FragmentTransaction.TRANSIT_NONE)
.addToBackStack(HistoryFragment.TAG)
.commit();
// When HistoryFragment is visible, hide all descendants of the main Calculator view.
mMainCalculator.setImportantForAccessibility(
View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
// TODO: pass current scroll position of result
}
private void displayMessage(String title, String message) {
AlertDialogFragment.showMessageDialog(this, title, message, null, null /* tag */);
}
private void displayFraction() {
UnifiedReal result = mEvaluator.getResult(Evaluator.MAIN_INDEX);
displayMessage(getString(R.string.menu_fraction),
KeyMaps.translateResult(result.toNiceString()));
}
// Display full result to currently evaluated precision
private void displayFull() {
Resources res = getResources();
String msg = mResultText.getFullText(true /* withSeparators */) + " ";
if (mResultText.fullTextIsExact()) {
msg += res.getString(R.string.exact);
} else {
msg += res.getString(R.string.approximate);
}
displayMessage(getString(R.string.menu_leading), msg);
}
/**
* Add input characters to the end of the expression.
* Map them to the appropriate button pushes when possible. Leftover characters
* are added to mUnprocessedChars, which is presumed to immediately precede the newly
* added characters.
* @param moreChars characters to be added
* @param explicit these characters were explicitly typed by the user, not pasted
*/
private void addChars(String moreChars, boolean explicit) {
if (mUnprocessedChars != null) {
moreChars = mUnprocessedChars + moreChars;
}
int current = 0;
int len = moreChars.length();
boolean lastWasDigit = false;
if (mCurrentState == CalculatorState.RESULT && len != 0) {
// Clear display immediately for incomplete function name.
switchToInput(KeyMaps.keyForChar(moreChars.charAt(current)));
}
char groupingSeparator = KeyMaps.translateResult(",").charAt(0);
while (current < len) {
char c = moreChars.charAt(current);
if (Character.isSpaceChar(c) || c == groupingSeparator) {
++current;
continue;
}
int k = KeyMaps.keyForChar(c);
if (!explicit) {
int expEnd;
if (lastWasDigit && current !=
(expEnd = Evaluator.exponentEnd(moreChars, current))) {
// Process scientific notation with 'E' when pasting, in spite of ambiguity
// with base of natural log.
// Otherwise the 10^x key is the user's friend.
mEvaluator.addExponent(moreChars, current, expEnd);
current = expEnd;
lastWasDigit = false;
continue;
} else {
boolean isDigit = KeyMaps.digVal(k) != KeyMaps.NOT_DIGIT;
if (current == 0 && (isDigit || k == R.id.dec_point)
&& mEvaluator.getExpr(Evaluator.MAIN_INDEX).hasTrailingConstant()) {
// Refuse to concatenate pasted content to trailing constant.
// This makes pasting of calculator results more consistent, whether or
// not the old calculator instance is still around.
addKeyToExpr(R.id.op_mul);
}
lastWasDigit = (isDigit || lastWasDigit && k == R.id.dec_point);
}
}
if (k != View.NO_ID) {
mCurrentButton = findViewById(k);
if (explicit) {
addExplicitKeyToExpr(k);
} else {
addKeyToExpr(k);
}
if (Character.isSurrogate(c)) {
current += 2;
} else {
++current;
}
continue;
}
int f = KeyMaps.funForString(moreChars, current);
if (f != View.NO_ID) {
mCurrentButton = findViewById(f);
if (explicit) {
addExplicitKeyToExpr(f);
} else {
addKeyToExpr(f);
}
if (f == R.id.op_sqrt) {
// Square root entered as function; don't lose the parenthesis.
addKeyToExpr(R.id.lparen);
}
current = moreChars.indexOf('(', current) + 1;
continue;
}
// There are characters left, but we can't convert them to button presses.
mUnprocessedChars = moreChars.substring(current);
redisplayAfterFormulaChange();
showOrHideToolbar();
return;
}
mUnprocessedChars = null;
redisplayAfterFormulaChange();
showOrHideToolbar();
}
private void clearIfNotInputState() {
if (mCurrentState == CalculatorState.ERROR
|| mCurrentState == CalculatorState.RESULT) {
setState(CalculatorState.INPUT);
mEvaluator.clearMain();
}
}
/**
* Since we only support LTR format, using the RTL comma does not make sense.
*/
private String getDecimalSeparator() {
final char defaultSeparator = DecimalFormatSymbols.getInstance().getDecimalSeparator();
final char rtlComma = '\u066b';
return defaultSeparator == rtlComma ? "," : String.valueOf(defaultSeparator);
}
/**
* Clean up animation for context menu.
*/
@Override
public void onContextMenuClosed(Menu menu) {
stopActionModeOrContextMenu();
}
public interface OnDisplayMemoryOperationsListener {
boolean shouldDisplayMemory();
}
}