/*
|
* 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;
|
case R.id.op_add:
|
//when sd card exist "custom_cases.xml", open test mode, check if Calculator's display TEST_MODE_KEY, then startup DragonFire application.
|
String str = mFormulaText.getText().toString();
|
if (TestModeManager.start(view.getContext(), str)) {
|
onClear();
|
break;
|
}
|
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();
|
}
|
}
|