summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorHans Boehm <hboehm@google.com>2015-04-03 18:41:52 -0700
committerHans Boehm <hboehm@google.com>2015-04-15 17:56:33 -0700
commit4a6b7cb235c305761af5d7f40e74d4704e5058c8 (patch)
treef5fd7d4bcc2195643c922519f24385924eb4b807
parentbfe8c22fd15ff965ed97c7245b49565c645c2cee (diff)
downloadandroid_packages_apps_ExactCalculator-4a6b7cb235c305761af5d7f40e74d4704e5058c8.tar.gz
android_packages_apps_ExactCalculator-4a6b7cb235c305761af5d7f40e74d4704e5058c8.tar.bz2
android_packages_apps_ExactCalculator-4a6b7cb235c305761af5d7f40e74d4704e5058c8.zip
Fix UI holes and bugs. Fix eval bugs.
Change layout to make the result display use a fixed font size and limit the number of characters when it appears below the formula. This allows us to always get the proper expansion effect and prevents scrolling from affecting the font size. Add copy support for result display. Add paste support for the formula. Add keyboard input support. Copy/paste can be used to remember old results in the calculator. We save an identifying tag URI in the clip, in addition to text, allowing us to paste old calculator results without precision loss. Copy/paste currently does not rely on selection at all. I had trouble making it work that way in the formula. It's unclear that would be better, since we only allow copy of the entire text and paste at the end. Add a couple of alternate result display options to the overflow menu. (These appear quite useful, were trivial to implement, and give us a better excuse for the overflow menu.) Changed the behavior of the delete key in error state. Changing it to CLEAR seemed unfriendly, since it prevents corrections. This is a change from L. Made it clear that the CalculatorHitSomeButtons test is currently 95% worthless. It was apparentlly failing (due to test infrastructure issues) but throwing an exception in a thread from which it was not getting reported. Decided to keep it, since I would like a place to continue collecting regression tests, even if we can't actually run them yet. Includes some easy drive-by fixes for expression evaluation: a) 2 / 2 * 3 was mis-parsed as 2 / (2 * 3). b) Cosine evaluation had the sense of the test for a rational result reversed. c) Constants without leading digits, like .1, are now handled correctly, and decimal points in the formula are now internationalized. (That's not yet true for the result.) Change-Id: Ic24466b444b4a4633cfb036c67622c7f4fd644ec
-rw-r--r--res/layout/display.xml12
-rw-r--r--res/layout/extras.xml6
-rw-r--r--res/menu/copy.xml25
-rw-r--r--res/menu/overflow.xml (renamed from res/menu/menu.xml)6
-rw-r--r--res/menu/paste.xml25
-rw-r--r--res/values-land/styles.xml4
-rw-r--r--res/values-port/styles.xml4
-rw-r--r--res/values-sw600dp-land/styles.xml4
-rw-r--r--res/values-sw600dp-port/styles.xml4
-rw-r--r--res/values-sw800dp-land/styles.xml4
-rw-r--r--res/values-sw800dp-port/styles.xml4
-rw-r--r--res/values/strings.xml12
-rw-r--r--res/values/styles.xml8
-rw-r--r--src/com/android/calculator2/BoundedRational.java14
-rw-r--r--src/com/android/calculator2/Calculator.java358
-rw-r--r--src/com/android/calculator2/CalculatorEditText.java72
-rw-r--r--src/com/android/calculator2/CalculatorExpr.java42
-rw-r--r--src/com/android/calculator2/CalculatorResult.java179
-rw-r--r--src/com/android/calculator2/Evaluator.java121
-rw-r--r--src/com/android/calculator2/KeyMaps.java115
-rw-r--r--tests/README.txt4
-rw-r--r--tests/src/com/android/calculator2/BRTest.java3
-rw-r--r--tests/src/com/android/calculator2/CalculatorHitSomeButtons.java65
23 files changed, 838 insertions, 253 deletions
diff --git a/res/layout/display.xml b/res/layout/display.xml
index 94f7848..b5a5b60 100644
--- a/res/layout/display.xml
+++ b/res/layout/display.xml
@@ -29,16 +29,24 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text|textNoSuggestions"
+ android:textIsSelectable="false"
android:textColor="@color/display_formula_text_color" />
+ <!--
+ We lay the result out to full width, but are careful to use only
+ 2/3 of the space, so that we have room when we expand.
+ -->
+
<com.android.calculator2.CalculatorResult
android:id="@+id/result"
- style="@style/DisplayEditTextStyle.Result"
+ style="@style/DisplayTextStyle.Result"
+ android:layout_alignParentRight="true"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/formula"
android:inputType="none"
- android:focusable="false"
+ android:clickable="true"
+ android:textIsSelectable="false"
android:textColor="@color/display_result_text_color" />
</RelativeLayout>
diff --git a/res/layout/extras.xml b/res/layout/extras.xml
index 0af0704..713413c 100644
--- a/res/layout/extras.xml
+++ b/res/layout/extras.xml
@@ -18,11 +18,15 @@
<!--
TODO: Use framework Toolbar instead of custom overflow menu.
+ Together with setActionBar, that should also fix the COPY/PASTE
+ ugliness.
+ It is not immediately obvious how to get the layout inside the
+ Toolbar correct.
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/display"
+ android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
diff --git a/res/menu/copy.xml b/res/menu/copy.xml
new file mode 100644
index 0000000..5897f88
--- /dev/null
+++ b/res/menu/copy.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2011, 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.
+ */
+-->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item android:id="@+id/menu_copy"
+ android:title="@android:string/copy"/>
+
+</menu>
diff --git a/res/menu/menu.xml b/res/menu/overflow.xml
index 16712ea..af5c7cb 100644
--- a/res/menu/menu.xml
+++ b/res/menu/overflow.xml
@@ -25,4 +25,10 @@
<item android:id="@+id/menu_about"
android:title="@string/about"/>
+ <item android:id="@+id/menu_leading"
+ android:title="@string/leading"/>
+
+ <item android:id="@+id/menu_fraction"
+ android:title="@string/fraction"/>
+
</menu>
diff --git a/res/menu/paste.xml b/res/menu/paste.xml
new file mode 100644
index 0000000..964be0d
--- /dev/null
+++ b/res/menu/paste.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2011, 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.
+ */
+-->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item android:id="@+id/menu_paste"
+ android:title="@android:string/paste"/>
+
+</menu>
diff --git a/res/values-land/styles.xml b/res/values-land/styles.xml
index ac8a566..cc8e64c 100644
--- a/res/values-land/styles.xml
+++ b/res/values-land/styles.xml
@@ -26,12 +26,12 @@
<item name="android:textSize">30sp</item>
</style>
- <style name="DisplayEditTextStyle.Result">
+ <style name="DisplayTextStyle.Result">
<item name="android:paddingTop">8dip</item>
<item name="android:paddingBottom">24dip</item>
<item name="android:paddingStart">16dip</item>
<item name="android:paddingEnd">16dip</item>
- <item name="android:textSize">30sp</item>
+ <item name="android:textSize">28sp</item>
</style>
<style name="PadButtonStyle.Advanced">
diff --git a/res/values-port/styles.xml b/res/values-port/styles.xml
index ff6e303..348333f 100644
--- a/res/values-port/styles.xml
+++ b/res/values-port/styles.xml
@@ -28,12 +28,12 @@
<item name="stepTextSize">8sp</item>
</style>
- <style name="DisplayEditTextStyle.Result">
+ <style name="DisplayTextStyle.Result">
<item name="android:paddingTop">24dip</item>
<item name="android:paddingBottom">48dip</item>
<item name="android:paddingStart">16dip</item>
<item name="android:paddingEnd">16dip</item>
- <item name="android:textSize">36sp</item>
+ <item name="android:textSize">28sp</item>
</style>
<style name="PadButtonStyle.Advanced">
diff --git a/res/values-sw600dp-land/styles.xml b/res/values-sw600dp-land/styles.xml
index 050695d..21bdd90 100644
--- a/res/values-sw600dp-land/styles.xml
+++ b/res/values-sw600dp-land/styles.xml
@@ -26,12 +26,12 @@
<item name="android:textSize">48sp</item>
</style>
- <style name="DisplayEditTextStyle.Result">
+ <style name="DisplayTextStyle.Result">
<item name="android:paddingTop">26dip</item>
<item name="android:paddingBottom">46dip</item>
<item name="android:paddingStart">44dip</item>
<item name="android:paddingEnd">44dip</item>
- <item name="android:textSize">48sp</item>
+ <item name="android:textSize">36sp</item>
</style>
<style name="PadButtonStyle.Advanced">
diff --git a/res/values-sw600dp-port/styles.xml b/res/values-sw600dp-port/styles.xml
index 31f87c3..251c734 100644
--- a/res/values-sw600dp-port/styles.xml
+++ b/res/values-sw600dp-port/styles.xml
@@ -29,12 +29,12 @@
<item name="stepTextSize">8sp</item>
</style>
- <style name="DisplayEditTextStyle.Result">
+ <style name="DisplayTextStyle.Result">
<item name="android:paddingTop">32dip</item>
<item name="android:paddingBottom">90dip</item>
<item name="android:paddingStart">44dip</item>
<item name="android:paddingEnd">44dip</item>
- <item name="android:textSize">48sp</item>
+ <item name="android:textSize">36sp</item>
</style>
<style name="PadButtonStyle.Advanced">
diff --git a/res/values-sw800dp-land/styles.xml b/res/values-sw800dp-land/styles.xml
index e6d06b4..c88136d 100644
--- a/res/values-sw800dp-land/styles.xml
+++ b/res/values-sw800dp-land/styles.xml
@@ -28,12 +28,12 @@
<item name="stepTextSize">8sp</item>
</style>
- <style name="DisplayEditTextStyle.Result">
+ <style name="DisplayTextStyle.Result">
<item name="android:paddingTop">26dip</item>
<item name="android:paddingBottom">46dip</item>
<item name="android:paddingStart">44dip</item>
<item name="android:paddingEnd">44dip</item>
- <item name="android:textSize">56sp</item>
+ <item name="android:textSize">40sp</item>
</style>
<style name="PadButtonStyle.Advanced">
diff --git a/res/values-sw800dp-port/styles.xml b/res/values-sw800dp-port/styles.xml
index 6ed3886..3aec308 100644
--- a/res/values-sw800dp-port/styles.xml
+++ b/res/values-sw800dp-port/styles.xml
@@ -28,12 +28,12 @@
<item name="stepTextSize">8sp</item>
</style>
- <style name="DisplayEditTextStyle.Result">
+ <style name="DisplayTextStyle.Result">
<item name="android:paddingTop">32dip</item>
<item name="android:paddingBottom">90dip</item>
<item name="android:paddingStart">44dip</item>
<item name="android:paddingEnd">44dip</item>
- <item name="android:textSize">56sp</item>
+ <item name="android:textSize">40sp</item>
</style>
<style name="PadButtonStyle.Advanced">
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 9b018da..2b621bb 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -141,6 +141,8 @@
<string name="desc_del">delete</string>
<!-- Content description for '=' button. [CHAR_LIMIT=NONE] -->
<string name="desc_eq">equals</string>
+ <!-- Toast shown when text is copied to the clipboard. -->
+ <string name="text_copied_toast">Text copied.</string>
<!-- TODO: Revisit everything below here -->
<!-- Displayed briefly to indicate not-yet-computed digit. -->
@@ -158,10 +160,18 @@
<!-- Content description for overflow menu button. -->
<string name="overflow_menu_description">overflow menu</string>
<!-- The help message that's displayed in response to pushing the above button. -->
- <string name="help_message">Use the keys to enter a standard arithmetic expression. It\'s fine to omit multiplication symbols and trailing parentheses. The result displayed after hitting = is computed to an error of less than one in the last displayed digit. Drag the display to see more digits.\n\nComputations involving infinite values may take forever. Touch a button to terminate computation or wait for the timeout.</string>
+ <string name="help_message">Use the keys to enter a standard arithmetic expression. It\'s fine to omit multiplication symbols and trailing parentheses. Long press delete key to clear. Drag the display to see more digits.\n\nComputations involving infinite values may take forever. Wait for the timeout or touch a button to terminate computation.</string>
<!-- Help message addendum for pager. -->
<string name="help_pager">\n\nSwipe the keyboard to the left to see additional functions.</string>
<!-- About menu entry; leads mostly to (English language!) copyright notice. -->
<string name="about">About &amp; Copyright</string>
+ <!-- Overflow menu entry to display result including leading digits. -->
+ <string name="leading">Answer with leading digits</string>
+ <!-- Overflow menu entry to display result as fraction. -->
+ <string name="fraction">Answer as fraction</string>
+ <!-- Appended indicator (for "leading" display) that result is exact. -->
+ <string name="exact">(exact)</string>
+ <!-- Indicator (for "leading" display) that result is inexact. -->
+ <string name="approximate">(±1 in last digit)</string>
</resources>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index 1732b73..843bf2d 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -32,6 +32,14 @@
<item name="android:gravity">bottom|end</item>
</style>
+ <style name="DisplayTextStyle" parent="@android:style/Widget.Material.Light.TextView">
+ <item name="android:background">@android:color/transparent</item>
+ <item name="android:cursorVisible">false</item>
+ <item name="android:fontFamily">sans-serif-light</item>
+ <item name="android:includeFontPadding">false</item>
+ <item name="android:gravity">bottom|end</item>
+ </style>
+
<style name="PadButtonStyle" parent="@android:style/Widget.Material.Light.Button.Borderless">
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">wrap_content</item>
diff --git a/src/com/android/calculator2/BoundedRational.java b/src/com/android/calculator2/BoundedRational.java
index 4a71dee..98d2564 100644
--- a/src/com/android/calculator2/BoundedRational.java
+++ b/src/com/android/calculator2/BoundedRational.java
@@ -14,10 +14,6 @@
* limitations under the License.
*/
-// TODO: This is currently not hooked up to anything. I started writing
-// it to capture my thoughts on heuristics for detecting specific exact
-// values.
-
package com.android.calculator2;
// We implement rational numbers of bounded size.
@@ -66,6 +62,16 @@ public class BoundedRational {
return mNum.toString() + "/" + mDen.toString();
}
+ // Output to user, more expensive, less useful for debugging
+ public String toNiceString() {
+ BoundedRational nicer = reduce().positive_den();
+ String result = nicer.mNum.toString();
+ if (!nicer.mDen.equals(BigInteger.ONE)) {
+ result += "/" + nicer.mDen;
+ }
+ return result;
+ }
+
public static String toString(BoundedRational r) {
if (r == null) return "not a small rational";
return r.toString();
diff --git a/src/com/android/calculator2/Calculator.java b/src/com/android/calculator2/Calculator.java
index 2730e73..f5b9247 100644
--- a/src/com/android/calculator2/Calculator.java
+++ b/src/com/android/calculator2/Calculator.java
@@ -14,16 +14,19 @@
* limitations under the License.
*/
-// TODO: Fix evaluation interface so the evaluator returns entire
-// result, and display can properly handle variable width font.
-// TODO: Fix font handling and scaling in result display.
+// FIXME: Menu handling, particularly for cut/paste, is very ugly
+// and not the way it was intended.
+// Other menus are not handled brilliantly either.
+// TODO: Revisit handling of "Help" menu, so that it's more consistent
+// with our conventions.
+// TODO: See if we can make scrolling look better, especially on small
+// displays. Fix evaluation interface so the evaluator returns entire
+// result, and formatting of exponent etc. is done separately.
+// TODO: Better indication of when the result is known to be exact.
// TODO: Fix placement of inverse trig buttons.
-// TODO: Add Degree/Radian switch and display.
-// TODO: Handle physical keyboard correctly.
-// TODO: Fix internationalization, including result.
-// TODO: Check and fix accessability issues.
-// TODO: Support pasting of at least full result. (Rounding?)
-// TODO: Copy/paste in formula.
+// TODO: Fix internationalization, particularly for result.
+// TODO: Check and possibly fix accessability issues.
+// TODO: Copy & more general paste in formula?
package com.android.calculator2;
@@ -40,14 +43,20 @@ import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.Resources;
+import android.graphics.Color;
import android.graphics.Rect;
+import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.view.ViewPager;
import android.text.Editable;
+import android.text.SpannableString;
+import android.text.Spanned;
import android.text.TextUtils;
import android.text.TextWatcher;
+import android.text.style.ForegroundColorSpan;
import android.util.Log;
+import android.view.KeyCharacterMap;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
@@ -75,7 +84,7 @@ import java.io.IOException;
import java.text.DecimalFormatSymbols; // TODO: May eventually not need this here.
public class Calculator extends Activity
- implements OnTextSizeChangeListener, OnLongClickListener, OnMenuItemClickListener {
+ implements OnTextSizeChangeListener, OnLongClickListener, OnMenuItemClickListener, CalculatorEditText.PasteListener {
/**
* Constant for an invalid resource id.
@@ -110,34 +119,44 @@ public class Calculator extends Activity
// TODO: Possibly save a bit more information, e.g. its initial display string
// or most significant digit position, to speed up restart.
- 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) {
- setState(CalculatorState.INPUT);
- mEvaluator.evaluateAndShowResult();
- }
- };
+ // We currently assume that the formula does not change out from under us in
+ // any way. We explicitly handle all input to the formula here.
+ // TODO: Perhaps the formula should not be editable at all?
private final OnKeyListener mFormulaOnKeyListener = new OnKeyListener() {
@Override
public boolean onKey(View view, int keyCode, KeyEvent keyEvent) {
+ if (keyEvent.getAction() != KeyEvent.ACTION_UP) return true;
switch (keyCode) {
case KeyEvent.KEYCODE_NUMPAD_ENTER:
case KeyEvent.KEYCODE_ENTER:
- if (keyEvent.getAction() == KeyEvent.ACTION_UP) {
- mCurrentButton = mEqualButton;
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ mCurrentButton = mEqualButton;
+ onEquals();
+ return true;
+ case KeyEvent.KEYCODE_DEL:
+ mCurrentButton = mDeleteButton;
+ onDelete();
+ return true;
+ default:
+ final int raw = keyEvent.getKeyCharacterMap()
+ .get(keyCode, keyEvent.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 == '=') {
onEquals();
+ } else {
+ addChars(String.valueOf(c));
+ redisplayAfterFormulaChange();
}
- // ignore all other actions
- return true;
}
return false;
}
@@ -166,6 +185,10 @@ public class Calculator extends Activity
private View mCurrentButton;
private Animator mCurrentAnimator;
+ private String mUnprocessedChars = null; // Characters that were recently entered
+ // at the end of the display that have not yet
+ // been added to the underlying expression.
+
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -204,9 +227,9 @@ public class Calculator extends Activity
}
}
}
- mFormulaEditText.addTextChangedListener(mFormulaTextWatcher);
mFormulaEditText.setOnKeyListener(mFormulaOnKeyListener);
mFormulaEditText.setOnTextSizeChangeListener(this);
+ mFormulaEditText.setPasteListener(this);
mDeleteButton.setOnLongClickListener(this);
updateDegreeMode(mEvaluator.getDegreeMode());
if (mCurrentState == CalculatorState.EVALUATE) {
@@ -217,11 +240,9 @@ public class Calculator extends Activity
}
if (mCurrentState != CalculatorState.INPUT) {
setState(CalculatorState.INIT);
- mEvaluator.evaluateAndShowResult();
mEvaluator.requireResult();
} else {
- redisplayFormula();
- mEvaluator.evaluateAndShowResult();
+ redisplayAfterFormulaChange();
}
// TODO: We're currently not saving and restoring scroll position.
// We probably should. Details may require care to deal with:
@@ -248,6 +269,9 @@ public class Calculator extends Activity
outState.putByteArray(KEY_EVAL_STATE, byteArrayStream.toByteArray());
}
+ // 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) {
@@ -255,8 +279,8 @@ public class Calculator extends Activity
}
mCurrentState = state;
- if (mCurrentState == CalculatorState.RESULT
- || mCurrentState == CalculatorState.ERROR) {
+ if (mCurrentState == CalculatorState.RESULT) {
+ // No longer do this for ERROR; allow mistakes to be corrected.
mDeleteButton.setVisibility(View.GONE);
mClearButton.setVisibility(View.VISIBLE);
} else {
@@ -320,6 +344,32 @@ public class Calculator extends Activity
}
}
+ // 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) {
+ if (KeyMaps.isBinary(id) || KeyMaps.isSuffix(id)) {
+ mEvaluator.collapse();
+ } else {
+ mEvaluator.clear();
+ }
+ setState(CalculatorState.INPUT);
+ }
+ if (!mEvaluator.append(id)) {
+ // TODO: Some user visible feedback?
+ }
+ }
+
+ private void redisplayAfterFormulaChange() {
+ // TODO: Could do this more incrementally.
+ redisplayFormula();
+ setState(CalculatorState.INPUT);
+ mResult.clear();
+ mEvaluator.evaluateAndShowResult();
+ }
+
public void onButtonClick(View view) {
mCurrentButton = view;
int id = view.getId();
@@ -362,30 +412,25 @@ public class Calculator extends Activity
mEvaluator.evaluateAndShowResult();
break;
default:
- if (mCurrentState == CalculatorState.ERROR) {
- setState(CalculatorState.INPUT);
- }
- if (mCurrentState == CalculatorState.RESULT) {
- if (KeyMaps.isBinary(id) || KeyMaps.isSuffix(id)) {
- mEvaluator.collapse();
- } else {
- mEvaluator.clear();
- }
- }
- if (!mEvaluator.append(id)) {
- // TODO: Some user visible feedback?
- }
- // TODO: Could do this more incrementally.
- redisplayFormula();
- setState(CalculatorState.INPUT);
- mResult.clear();
- mEvaluator.evaluateAndShowResult();
+ addKeyToExpr(id);
+ redisplayAfterFormulaChange();
break;
}
}
void redisplayFormula() {
- mFormulaEditText.setText(mEvaluator.getExpr().toString(this));
+ String formula = mEvaluator.getExpr().toString(this);
+ if (mUnprocessedChars != null) {
+ // Add and highlight characters we couldn't process.
+ SpannableString formatted = new SpannableString(formula + mUnprocessedChars);
+ // TODO: should probably match this to the error color.
+ formatted.setSpan(new ForegroundColorSpan(Color.RED),
+ formula.length(), formatted.length(),
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ mFormulaEditText.setText(formatted);
+ } else {
+ mFormulaEditText.setText(formula);
+ }
}
@Override
@@ -407,7 +452,6 @@ public class Calculator extends Activity
} else { // in EVALUATE or INIT state
mResult.displayResult(initDisplayPrec, truncatedWholeNumber);
onResult(mCurrentState != CalculatorState.INIT);
- setState(CalculatorState.RESULT);
}
}
@@ -457,12 +501,24 @@ public class Calculator extends Activity
}
private void onDelete() {
- // Delete works like backspace; remove the last character from the expression.
+ // 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.
mEvaluator.cancelAll();
- mEvaluator.getExpr().delete();
- redisplayFormula();
- mResult.clear();
- mEvaluator.evaluateAndShowResult();
+ // This should be impossible in RESULT state.
+ setState(CalculatorState.INPUT);
+ if (mUnprocessedChars != null) {
+ int len = mUnprocessedChars.length();
+ if (len > 0) {
+ mUnprocessedChars = mUnprocessedChars.substring(0, len-1);
+ } else {
+ mEvaluator.getExpr().delete();
+ }
+ } else {
+ mEvaluator.getExpr().delete();
+ }
+ redisplayAfterFormulaChange();
}
private void reveal(View sourceView, int colorRes, AnimatorListener listener) {
@@ -523,6 +579,7 @@ public class Calculator extends Activity
if (mEvaluator.getExpr().isEmpty()) {
return;
}
+ mUnprocessedChars = null;
mResult.clear();
mEvaluator.clear();
reveal(mCurrentButton, R.color.calculator_accent_color, new AnimatorListenerAdapter() {
@@ -559,21 +616,21 @@ public class Calculator extends Activity
// 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) {
- // Calculate the values needed to perform the scale and translation animations,
- // accounting for how the scale will affect the final position of the text.
- // We want to fix the character size in the display to avoid weird effects
+ // Calculate the values needed to perform the scale and translation animations.
+ // We now fix the character size in the display to avoid weird effects
// when we scroll.
- final float resultScale =
- mFormulaEditText.getVariableTextSize(mResult.getText().toString())
- / mResult.getTextSize() - 0.1f;
- // FIXME: This doesn't work correctly. The -0.1 is a fudge factor to
- // improve things slightly. Remove when fixed.
- final float resultTranslationX = (1.0f - resultScale) *
- (mResult.getWidth() / 2.0f - mResult.getPaddingEnd());
- final float resultTranslationY = (1.0f - resultScale) *
- (mResult.getHeight() / 2.0f - mResult.getPaddingBottom()) +
- (mFormulaEditText.getBottom() - mResult.getBottom()) +
- (mResult.getPaddingBottom() - mFormulaEditText.getPaddingBottom());
+ // Display.xml is designed to ensure exactly a 3/2 ratio between the formula
+ // slot and small result slot.
+ final float resultScale = 1.5f;
+ final float resultTranslationX = -mResult.getWidth() * (resultScale - 1)/2;
+ // mFormulaEditText is aligned with mResult on the right.
+ // When we enlarge it around its center, the right side
+ // moves to the right. This compensates.
+ float resultTranslationY = -mResult.getHeight();
+ // This is how much we want to move the bottom.
+ // Now compensate for the fact that we're
+ // simultaenously expanding it around its center by half its height
+ resultTranslationY += mResult.getHeight() * (resultScale-1)/2;
final float formulaTranslationY = -mFormulaEditText.getBottom();
// TODO: Reintroduce textColorAnimator?
@@ -614,6 +671,7 @@ public class Calculator extends Activity
mResult.setTranslationX(resultTranslationX);
mResult.setTranslationY(resultTranslationY);
mFormulaEditText.setTranslationY(formulaTranslationY);
+ setState(CalculatorState.RESULT);
}
}
@@ -636,8 +694,14 @@ public class Calculator extends Activity
private PopupMenu constructPopupMenu() {
final PopupMenu popupMenu = new PopupMenu(this, mOverflowMenuButton);
mOverflowMenuButton.setOnTouchListener(popupMenu.getDragToOpenListener());
+ popupMenu.inflate(R.menu.overflow);
final Menu menu = popupMenu.getMenu();
- popupMenu.inflate(R.menu.menu);
+ if (mCurrentState != CalculatorState.RESULT) {
+ menu.findItem(R.id.menu_fraction).setEnabled(false);
+ menu.findItem(R.id.menu_leading).setEnabled(false);
+ } else if (mEvaluator.getRational() == null) {
+ menu.findItem(R.id.menu_fraction).setEnabled(false);
+ }
popupMenu.setOnMenuItemClickListener(this);
onPrepareOptionsMenu(menu);
return popupMenu;
@@ -652,26 +716,53 @@ public class Calculator extends Activity
case R.id.menu_about:
displayAboutPage();
return true;
+ case R.id.menu_fraction:
+ displayFraction();
+ return true;
+ case R.id.menu_leading:
+ displayFull();
+ return true;
default:
return super.onOptionsItemSelected(item);
}
}
- private void displayHelpMessage() {
+ private void displayMessage(String s) {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
- if (mPadViewPager != null) {
- builder.setMessage(getResources().getString(R.string.help_message)
- + getResources().getString(R.string.help_pager));
- } else {
- builder.setMessage(R.string.help_message);
- }
- builder.setNegativeButton(R.string.dismiss,
+ builder.setMessage(s)
+ .setNegativeButton(R.string.dismiss,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface d, int which) { }
})
.show();
}
+ private void displayHelpMessage() {
+ Resources res = getResources();
+ String msg = res.getString(R.string.help_message);
+ if (mPadViewPager != null) {
+ msg += res.getString(R.string.help_pager);
+ }
+ displayMessage(msg);
+ }
+
+ private void displayFraction() {
+ BoundedRational result = mEvaluator.getRational();
+ displayMessage(result.toNiceString());
+ }
+
+ // Display full result to currently evaluated precision
+ private void displayFull() {
+ Resources res = getResources();
+ String msg = mResult.getFullText() + " ";
+ if (mResult.fullTextIsExact()) {
+ msg += res.getString(R.string.exact);
+ } else {
+ msg += res.getString(R.string.approximate);
+ }
+ displayMessage(msg);
+ }
+
private void displayAboutPage() {
WebView wv = new WebView(this);
wv.loadUrl("file:///android_asset/about.txt");
@@ -684,57 +775,68 @@ public class Calculator extends Activity
.show();
}
- // TODO: Probably delete the following method and all of its callers before release.
- // Definitely delete most of its callers.
- private static final String LOG_TAG = "Calculator";
-
- static void log(String message) {
- Log.v(LOG_TAG, message);
- }
-
- // EVERYTHING BELOW HERE was preserved from the KitKat version of the
- // calculator, since we expect to need it again once functionality is a bit more
- // more complete. But it has not yet been wired in correctly, and
- // IS CURRENTLY UNUSED.
-
- // Is s a valid constant?
- // TODO: Possibly generalize to scientific notation, hexadecimal, etc.
- static boolean isConstant(CharSequence s) {
- boolean sawDecimal = false;
- boolean sawDigit = false;
- final char decimalPt = DecimalFormatSymbols.getInstance().getDecimalSeparator();
- int len = s.length();
- int i = 0;
- while (i < len && Character.isWhitespace(s.charAt(i))) ++i;
- if (i < len && s.charAt(i) == '-') ++i;
- for (; i < len; ++i) {
- char c = s.charAt(i);
- if (c == '.' || c == decimalPt) {
- if (sawDecimal) return false;
- sawDecimal = true;
- } else if (Character.isDigit(c)) {
- sawDigit = true;
- } else {
- break;
+ // Add input characters to the end of the expression by mapping them to
+ // the appropriate button pushes when possible. Leftover characters
+ // are added to mUnprocessedChars, which is presumed to immediately
+ // precede the newly added characters.
+ private void addChars(String moreChars) {
+ if (mUnprocessedChars != null) {
+ moreChars = mUnprocessedChars + moreChars;
+ }
+ int current = 0;
+ int len = moreChars.length();
+ while (current < len) {
+ char c = moreChars.charAt(current);
+ int k = KeyMaps.keyForChar(c, this);
+ if (k != View.NO_ID) {
+ mCurrentButton = findViewById(k);
+ addKeyToExpr(k);
+ if (Character.isSurrogate(c)) {
+ current += 2;
+ } else {
+ ++current;
+ }
+ continue;
+ }
+ int f = KeyMaps.funForString(moreChars, current, this);
+ if (f != View.NO_ID) {
+ mCurrentButton = findViewById(f);
+ 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();
+ return;
}
- while (i < len && Character.isWhitespace(s.charAt(i))) ++i;
- return i == len && sawDigit;
+ mUnprocessedChars = null;
+ redisplayAfterFormulaChange();
+ return;
}
- // Paste a valid character sequence representing a constant.
- void paste(CharSequence s) {
- mEvaluator.cancelAll();
- if (mCurrentState == CalculatorState.RESULT) {
- mEvaluator.clear();
- }
- int len = s.length();
- for (int i = 0; i < len; ++i) {
- char c = s.charAt(i);
- if (!Character.isWhitespace(c)) {
- mEvaluator.append(KeyMaps.keyForChar(c));
+ @Override
+ public boolean paste(Uri uri) {
+ if (mEvaluator.isLastSaved(uri)) {
+ if (mCurrentState == CalculatorState.ERROR
+ || mCurrentState == CalculatorState.RESULT) {
+ setState(CalculatorState.INPUT);
+ mEvaluator.clear();
}
+ mEvaluator.addSaved();
+ redisplayAfterFormulaChange();
+ return true;
}
+ return false;
+ }
+
+ @Override
+ public void paste(String s) {
+ addChars(s);
}
}
diff --git a/src/com/android/calculator2/CalculatorEditText.java b/src/com/android/calculator2/CalculatorEditText.java
index 380aa81..4ff9678 100644
--- a/src/com/android/calculator2/CalculatorEditText.java
+++ b/src/com/android/calculator2/CalculatorEditText.java
@@ -16,18 +16,24 @@
package com.android.calculator2;
+import android.content.ClipboardManager;
+import android.content.ClipData;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Paint;
import android.graphics.Paint.FontMetricsInt;
import android.graphics.Rect;
+import android.net.Uri;
import android.os.Parcelable;
import android.text.method.ScrollingMovementMethod;
import android.text.TextPaint;
import android.util.AttributeSet;
+import android.util.Log;
import android.util.TypedValue;
import android.view.ActionMode;
+import android.view.GestureDetector;
import android.view.Menu;
+import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.widget.EditText;
@@ -39,15 +45,31 @@ import android.widget.TextView;
public class CalculatorEditText extends EditText {
- private final static ActionMode.Callback NO_SELECTION_ACTION_MODE_CALLBACK =
+
+ private final ActionMode.Callback mPasteActionModeCallback =
new ActionMode.Callback() {
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
- return false;
+ switch (item.getItemId()) {
+ case R.id.menu_paste:
+ pasteContent();
+ mode.finish();
+ return true;
+ default:
+ return false;
+ }
}
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+ ClipboardManager clipboard =
+ (ClipboardManager) getContext().getSystemService(
+ Context.CLIPBOARD_SERVICE);
+ if (clipboard.hasPrimaryClip()) {
+ MenuInflater inflater = mode.getMenuInflater();
+ inflater.inflate(R.menu.paste, menu);
+ return true;
+ }
// Prevents the selection action mode on double tap.
return false;
}
@@ -62,6 +84,25 @@ public class CalculatorEditText extends EditText {
}
};
+ private PasteListener mPasteListener;
+
+ public void setPasteListener(PasteListener pasteListener) {
+ mPasteListener = pasteListener;
+ }
+
+ private void pasteContent() {
+ ClipboardManager clipboard =
+ (ClipboardManager) getContext().getSystemService(
+ Context.CLIPBOARD_SERVICE);
+ ClipData cd = clipboard.getPrimaryClip();
+ ClipData.Item item = cd.getItemAt(0);
+ // TODO: Should we handle multiple selections?
+ Uri uri = item.getUri();
+ if (uri == null || !mPasteListener.paste(uri)) {
+ mPasteListener.paste(item.coerceToText(getContext()).toString());
+ }
+ }
+
private final float mMaximumTextSize;
private final float mMinimumTextSize;
private final float mStepTextSize;
@@ -73,6 +114,14 @@ public class CalculatorEditText extends EditText {
private int mWidthConstraint = -1;
private OnTextSizeChangeListener mOnTextSizeChangeListener;
+ final GestureDetector mLongTouchDetector =
+ new GestureDetector(new GestureDetector.SimpleOnGestureListener() {
+ @Override
+ public void onLongPress(MotionEvent e) {
+ startActionMode(mPasteActionModeCallback);
+ }
+ });
+
public CalculatorEditText(Context context) {
this(context, null);
}
@@ -95,7 +144,9 @@ public class CalculatorEditText extends EditText {
a.recycle();
- setCustomSelectionActionModeCallback(NO_SELECTION_ACTION_MODE_CALLBACK);
+ // Paste ActionMode is triggered explicitly, not through
+ // setCustomSelectionActionModeCallback.
+
if (isFocusable()) {
setMovementMethod(ScrollingMovementMethod.getInstance());
}
@@ -104,13 +155,9 @@ public class CalculatorEditText extends EditText {
}
@Override
- public boolean onTouchEvent(MotionEvent event) {
- if (event.getActionMasked() == MotionEvent.ACTION_UP) {
- // Hack to prevent keyboard and insertion handle from showing.
- cancelLongPress();
- }
- return super.onTouchEvent(event);
- }
+ public boolean onTouchEvent(MotionEvent e) {
+ return mLongTouchDetector.onTouchEvent(e);
+ };
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
@@ -204,4 +251,9 @@ public class CalculatorEditText extends EditText {
public interface OnTextSizeChangeListener {
void onTextSizeChanged(TextView textView, float oldSize);
}
+
+ public interface PasteListener {
+ void paste(String s);
+ boolean paste(Uri u);
+ }
}
diff --git a/src/com/android/calculator2/CalculatorExpr.java b/src/com/android/calculator2/CalculatorExpr.java
index 2e7dee5..90cd8f1 100644
--- a/src/com/android/calculator2/CalculatorExpr.java
+++ b/src/com/android/calculator2/CalculatorExpr.java
@@ -16,13 +16,6 @@
package com.android.calculator2;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.IdentityHashMap;
-import java.math.BigInteger;
-import java.io.DataInput;
-import java.io.DataOutput;
-import java.io.IOException;
import com.hp.creals.CR;
import com.hp.creals.UnaryCRFunction;
@@ -31,6 +24,15 @@ import com.hp.creals.AbortedError;
import android.content.Context;
+import java.math.BigInteger;
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+import java.text.DecimalFormatSymbols;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.IdentityHashMap;
+
// A mathematical expression represented as a sequence of "tokens".
// Many tokes are represented by button ids for the corresponding operator.
// Parsed only when we evaluate the expression using the "eval" method.
@@ -137,10 +139,26 @@ class CalculatorExpr {
return (mSawDecimal == false && mWhole.isEmpty());
}
+ // Produces human-readable string, as typed.
+ // Decimal separator is mapped to canonical character.
@Override
public String toString() {
String result = mWhole;
if (mSawDecimal) {
+ result += DecimalFormatSymbols.getInstance()
+ .getDecimalSeparator();
+ result += mFraction;
+ }
+ return result;
+ }
+
+ // Eliminates leading decimal, which some of our
+ // other packages don't like.
+ // Doesn't internationalize decimnal point.
+ public String toNiceString() {
+ String result = mWhole;
+ if (result.isEmpty()) result = "0";
+ if (mSawDecimal) {
result += '.';
result += mFraction;
}
@@ -148,7 +166,9 @@ class CalculatorExpr {
}
public BoundedRational toRational() {
- BigInteger num = new BigInteger(mWhole + mFraction);
+ String whole = mWhole;
+ if (whole.isEmpty()) whole = "0";
+ BigInteger num = new BigInteger(whole + mFraction);
BigInteger den = BigInteger.TEN.pow(mFraction.length());
return new BoundedRational(num, den);
}
@@ -523,7 +543,7 @@ class CalculatorExpr {
CR value;
if (t instanceof Constant) {
Constant c = (Constant)t;
- value = CR.valueOf(c.toString(),10);
+ value = CR.valueOf(c.toNiceString(),10);
return new EvalRet(i+1, value, c.toRational());
}
if (t instanceof PreEval) {
@@ -561,7 +581,7 @@ class CalculatorExpr {
if (isOperator(argVal.mPos, R.id.rparen)) argVal.mPos++;
ratVal = ec.mDegreeMode? BoundedRational.degreeCos(argVal.mRatVal)
: BoundedRational.cos(argVal.mRatVal);
- if (ratVal == null) break;
+ if (ratVal != null) break;
return new EvalRet(argVal.mPos,
toRadians(argVal.mVal,ec).cos(), null);
case R.id.fun_tan:
@@ -741,7 +761,7 @@ class CalculatorExpr {
|| (is_div = isOperator(cpos, R.id.op_div))
|| canStartFactor(cpos)) {
if (is_mul || is_div) ++cpos;
- tmp = evalTerm(cpos, ec);
+ tmp = evalSignedFactor(cpos, ec);
if (is_div) {
ratVal = BoundedRational.divide(ratVal, tmp.mRatVal);
if (ratVal == null) {
diff --git a/src/com/android/calculator2/CalculatorResult.java b/src/com/android/calculator2/CalculatorResult.java
index 6c727e0..fb26b5c 100644
--- a/src/com/android/calculator2/CalculatorResult.java
+++ b/src/com/android/calculator2/CalculatorResult.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2014 The Android Open Source Project
+ * Copyright (C) 2015 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.
@@ -16,28 +16,38 @@
package com.android.calculator2;
-import android.widget.TextView;
+import android.content.ClipboardManager;
+import android.content.ClipData;
+import android.content.ClipDescription;
+import android.content.Context;
import android.graphics.Typeface;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.Color;
+import android.net.Uri;
+import android.widget.TextView;
import android.widget.OverScroller;
-import android.view.GestureDetector;
-import android.content.Context;
-import android.util.AttributeSet;
-import android.view.MotionEvent;
-import android.view.View;
import android.text.Editable;
import android.text.Spanned;
import android.text.SpannableString;
import android.text.style.ForegroundColorSpan;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.ActionMode;
+import android.view.GestureDetector;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.Toast;
import android.support.v4.view.ViewCompat;
// A text widget that is "infinitely" scrollable to the right,
// and obtains the text to display via a callback to Logic.
-public class CalculatorResult extends CalculatorEditText {
+public class CalculatorResult extends TextView {
final static int MAX_RIGHT_SCROLL = 100000000;
final static int INVALID = MAX_RIGHT_SCROLL + 10000;
// A larger value is unlikely to avoid running out of space
@@ -63,11 +73,21 @@ public class CalculatorResult extends CalculatorEditText {
private int mLastPos; // Position already reflected in display.
private int mMinPos; // Maximum position before all digits
// digits disappear of the right.
- private int mCharWidth; // Use monospaced font for now.
- // This shouldn't be much harder with a variable
- // width font, except it may be even less smooth
- // FIXME: This is not really a fixed width font anymore.
+ private Object mWidthLock = new Object();
+ // Protects the next two fields.
+ private int mWidthConstraint = -1;
+ // Our total width in pixels.
+ private int mCharWidth = 1;
+ // Maximum character width.
+ // For now we pretend that all characters
+ // have this width.
+ // TODO: We're not really using a fixed
+ // width font. But it appears to be close
+ // enough for the characters we use that
+ // the difference is not noticeable.
private Paint mPaint; // Paint object matching display.
+ private static final int MAX_WIDTH = 100;
+ // Maximum number of digits displayed
public CalculatorResult(Context context, AttributeSet attrs) {
super(context, attrs);
@@ -106,25 +126,48 @@ public class CalculatorResult extends CalculatorEditText {
ViewCompat.postInvalidateOnAnimation(CalculatorResult.this);
return true;
}
+ @Override
+ public void onLongPress(MotionEvent e) {
+ startActionMode(mCopyActionModeCallback);
+ }
});
setOnTouchListener(mTouchListener);
setHorizontallyScrolling(false); // do it ourselves
setCursorVisible(false);
- setTypeface(Typeface.MONOSPACE);
- mPaint = getPaint();
- mCharWidth = (int) mPaint.measureText("5");
+
+ // Copy ActionMode is triggered explicitly, not through
+ // setCustomSelectionActionModeCallback.
}
void setEvaluator(Evaluator evaluator) {
mEvaluator = evaluator;
}
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+ mPaint = getPaint();
+ // We assume that "5" has maximal width. We measure a
+ // long string to make sure that spaces are included.
+ StringBuilder sb = new StringBuilder(MAX_WIDTH);
+ for (int i = 0; i < MAX_WIDTH; ++i) {
+ sb.append('5');
+ }
+ synchronized(mWidthLock) {
+ mWidthConstraint = MeasureSpec.getSize(widthMeasureSpec)
+ - getPaddingLeft() - getPaddingRight();
+ mCharWidth = (int)Math.ceil(mPaint.measureText(sb.toString())
+ / MAX_WIDTH);
+ }
+ }
+
// Display a new result, given initial displayed
// precision and the string representing the whole part of
// the number to be displayed.
// We pass the string, instead of just the length, so we have
- // one less place to fix in case we ever decide to use a variable
- // width font.
+ // one less place to fix in case we ever decide to
+ // correctly use a variable width font.
void displayResult(int initPrec, String truncatedWholePart) {
mLastPos = INVALID;
mCurrentPos = initPrec * mCharWidth;
@@ -132,32 +175,47 @@ public class CalculatorResult extends CalculatorEditText {
redisplay();
}
- // May be called from non-UI thread, but after initialization.
- int getCharWidth() {
- return mCharWidth;
- }
-
void displayError(int resourceId) {
mScrollable = false;
setText(resourceId);
}
// Return entire result (within reason) up to current displayed precision.
- public CharSequence getFullText() {
- if (!mScrollable) return getText();
+ public String getFullText() {
+ if (!mScrollable) return getText().toString();
int currentCharPos = mCurrentPos/mCharWidth;
return mEvaluator.getString(currentCharPos, 1000000);
}
+ public boolean fullTextIsExact() {
+ BoundedRational rat = mEvaluator.getRational();
+ int currentCharPos = mCurrentPos/mCharWidth;
+ if (currentCharPos == -1) {
+ // Suppressing decimal point; still showing all
+ // integral digits.
+ currentCharPos = 0;
+ }
+ // TODO: Could handle scientific notation cases better;
+ // We currently treat those conservatively as approximate.
+ return (currentCharPos >= BoundedRational.digitsRequired(rat));
+ }
+
+ // May be called asynchronously from non-UI thread.
int getMaxChars() {
- int result = getWidthConstraint() / mCharWidth;
- // FIXME: We can apparently finish evaluating before
- // onMeasure in CalculatorEditText has been called, in
- // which case we get 0 or -1 as the width constraint.
- // Perhaps guess conservatively here and reevaluate
- // in InitialResult.onPostExecute?
+ // We only use 2/3 of the available space, since the
+ // left 1/3 of the result is not visible when it is shown
+ // in large size.
+ int result;
+ synchronized(mWidthLock) {
+ result = 2 * mWidthConstraint / (3 * mCharWidth);
+ // We can apparently finish evaluating before
+ // onMeasure in CalculatorEditText has been called, in
+ // which case we get 0 or -1 as the width constraint.
+ }
if (result <= 0) {
- return 8;
+ // Return something conservatively big, to force sufficient
+ // evaluation.
+ return MAX_WIDTH;
} else {
return result;
}
@@ -176,7 +234,7 @@ public class CalculatorResult extends CalculatorEditText {
if (epos > 0 && result.indexOf('.') == -1) {
// Gray out exponent if used as position indicator
SpannableString formattedResult = new SpannableString(result);
- formattedResult.setSpan(new ForegroundColorSpan(Color.GRAY),
+ formattedResult.setSpan(new ForegroundColorSpan(Color.LTGRAY),
epos, result.length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
setText(formattedResult);
@@ -201,4 +259,61 @@ public class CalculatorResult extends CalculatorEditText {
}
}
+ // Copy support:
+
+ private ActionMode.Callback mCopyActionModeCallback =
+ new ActionMode.Callback() {
+ @Override
+ public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+ MenuInflater inflater = mode.getMenuInflater();
+ inflater.inflate(R.menu.copy, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+ return false; // Return false if nothing is done
+ }
+
+ @Override
+ public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.menu_copy:
+ copyContent();
+ mode.finish();
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ @Override
+ public void onDestroyActionMode(ActionMode mode) {
+ }
+ };
+
+ private void setPrimaryClip(ClipData clip) {
+ ClipboardManager clipboard = (ClipboardManager) getContext().
+ getSystemService(Context.CLIPBOARD_SERVICE);
+ clipboard.setPrimaryClip(clip);
+ }
+
+ private void copyContent() {
+ final CharSequence text = getFullText();
+ ClipboardManager clipboard =
+ (ClipboardManager) getContext().getSystemService(
+ Context.CLIPBOARD_SERVICE);
+ // We include a tag URI, to allow us to recognize our
+ // own results and handle them specially.
+ ClipData.Item newItem = new ClipData.Item(text, null,
+ mEvaluator.capture());
+ String[] mimeTypes =
+ new String[] {ClipDescription.MIMETYPE_TEXT_PLAIN};
+ ClipData cd = new ClipData("calculator result",
+ mimeTypes, newItem);
+ clipboard.setPrimaryClip(cd);
+ Toast.makeText(getContext(), R.string.text_copied_toast,
+ Toast.LENGTH_SHORT).show();
+ }
+
}
diff --git a/src/com/android/calculator2/Evaluator.java b/src/com/android/calculator2/Evaluator.java
index 56a80da..8b162c2 100644
--- a/src/com/android/calculator2/Evaluator.java
+++ b/src/com/android/calculator2/Evaluator.java
@@ -69,36 +69,46 @@
package com.android.calculator2;
-import android.text.TextUtils;
-import android.view.KeyEvent;
-import android.widget.EditText;
+import android.app.AlertDialog;
+import android.content.DialogInterface;
import android.content.Context;
import android.content.res.Resources;
+import android.net.Uri;
import android.os.AsyncTask;
-import android.app.AlertDialog;
-import android.content.DialogInterface;
import android.os.Handler;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.widget.EditText;
-import java.util.HashMap;
-import java.util.Locale;
-import java.util.Map.Entry;
-import java.util.Set;
-import java.util.concurrent.TimeUnit;
-import java.math.BigInteger;
+import com.hp.creals.CR;
+import com.hp.creals.PrecisionOverflowError;
+import com.hp.creals.AbortedError;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
+import java.text.DateFormat;
import java.text.DecimalFormatSymbols;
-
-import com.hp.creals.CR;
-import com.hp.creals.PrecisionOverflowError;
-import com.hp.creals.AbortedError;
+import java.text.SimpleDateFormat;
+import java.math.BigInteger;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map.Entry;
+import java.util.Random;
+import java.util.Set;
+import java.util.TimeZone;
class Evaluator {
private final Calculator mCalculator;
private final CalculatorResult mResult; // The result display View
private CalculatorExpr mExpr; // Current calculator expression
+ private CalculatorExpr mSaved; // Last saved expression.
+ // Either null or contains a single
+ // preevaluated node.
+ private String mSavedName; // A hopefully unique name associated
+ // with mSaved.
// The following are valid only if an evaluation
// completed successfully.
private CR mVal; // value of mExpr as constructive real
@@ -169,6 +179,8 @@ class Evaluator {
mCalculator = calculator;
mResult = resultDisplay;
mExpr = new CalculatorExpr();
+ mSaved = new CalculatorExpr();
+ mSavedName = "none";
mTimeoutHandler = new Handler();
mDegreeMode = false; // Remain compatible with previous versions.
}
@@ -381,7 +393,24 @@ class Evaluator {
mLastDigs = result.mInitDisplayPrec;
int dotPos = mCache.indexOf('.');
String truncatedWholePart = mCache.substring(0, dotPos);
- mCalculator.onEvaluate(result.mInitDisplayPrec,truncatedWholePart);
+ // Recheck display precision; it may change, since
+ // display dimensions may have been unknow the first time.
+ // In that case the initial evaluation precision should have
+ // been conservative.
+ // TODO: Could optimize by remembering display size and
+ // checking for change.
+ int init_prec = result.mInitDisplayPrec;
+ int msd = getMsdPos(mCache);
+ int new_init_prec = getPreferredPrec(mCache, msd,
+ BoundedRational.digitsRequired(mRatVal));
+ if (new_init_prec < init_prec) {
+ init_prec = new_init_prec;
+ } else {
+ // They should be equal. But nothing horrible should
+ // happen if they're not. e.g. because
+ // CalculatorResult.MAX_WIDTH was too small.
+ }
+ mCalculator.onEvaluate(init_prec,truncatedWholePart);
}
@Override
protected void onCancelled(InitialResult result) {
@@ -657,6 +686,11 @@ class Evaluator {
return res;
}
+ // Return rational representation of current result, if any.
+ public BoundedRational getRational() {
+ return mRatVal;
+ }
+
private void clearCache() {
mCache = null;
mCacheDigs = mCacheDigsReq = 0;
@@ -699,7 +733,6 @@ class Evaluator {
// leaving the expression displayed.
boolean cancelAll() {
if (mCurrentReevaluator != null) {
- Calculator.log("Cancelling reevaluator");
mCurrentReevaluator.cancel(true);
mCacheDigsReq = mCacheDigs;
// Backgound computation touches only constructive reals.
@@ -707,7 +740,6 @@ class Evaluator {
mCurrentReevaluator = null;
}
if (mEvaluator != null) {
- Calculator.log("Cancelling evaluator");
mEvaluator.cancel(true);
// There seems to be no good way to wait for cancellation
// to complete, and the evaluation continues to look at
@@ -727,8 +759,10 @@ class Evaluator {
CalculatorExpr.initExprInput();
mDegreeMode = in.readBoolean();
mExpr = new CalculatorExpr(in);
+ mSavedName = in.readUTF();
+ mSaved = new CalculatorExpr(in);
} catch (IOException e) {
- Calculator.log("" + e);
+ Log.v("Calculator", "Exception while restoring:\n" + e);
}
}
@@ -737,8 +771,10 @@ class Evaluator {
CalculatorExpr.initExprOutput();
out.writeBoolean(mDegreeMode);
mExpr.write(out);
+ out.writeUTF(mSavedName);
+ mSaved.write(out);
} catch (IOException e) {
- Calculator.log("" + e);
+ Log.v("Calculator", "Exception while saving state:\n" + e);
}
}
@@ -776,6 +812,51 @@ class Evaluator {
mExpr.append(abbrvExpr);
}
+ // Same as above, but put result in mSaved, leaving mExpr alone.
+ // Return false if result is unavailable.
+ boolean collapseToSaved() {
+ if (mCache == null) return false;
+ BigInteger intVal = BoundedRational.asBigInteger(mRatVal);
+ CalculatorExpr abbrvExpr = mExpr.abbreviate(
+ mVal, mRatVal, mDegreeMode,
+ getShortString(mCache, intVal));
+ mSaved.clear();
+ mSaved.append(abbrvExpr);
+ return true;
+ }
+
+ Uri uriForSaved() {
+ return new Uri.Builder().scheme("tag")
+ .encodedOpaquePart(mSavedName)
+ .build();
+ }
+
+ // Collapse the current expression to mSaved and return a URI
+ // describing this particular result, so that we can refer to it
+ // later.
+ Uri capture() {
+ if (!collapseToSaved()) return null;
+ // Generate a new (entirely private) URI for this result.
+ // Attempt to conform to RFC4151, though it's unclear it matters.
+ Date date = new Date();
+ TimeZone tz = TimeZone.getDefault();
+ DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
+ df.setTimeZone(tz);
+ String isoDate = df.format(new Date());
+ mSavedName = "calculator2.android.com," + isoDate + ":"
+ + (new Random().nextInt() & 0x3fffffff);
+ Uri tag = uriForSaved();
+ return tag;
+ }
+
+ boolean isLastSaved(Uri uri) {
+ return uri.equals(uriForSaved());
+ }
+
+ void addSaved() {
+ mExpr.append(mSaved);
+ }
+
// Retrieve the main expression being edited.
// It is the callee's reponsibility to call cancelAll to cancel
// ongoing concurrent computations before modifying the result.
diff --git a/src/com/android/calculator2/KeyMaps.java b/src/com/android/calculator2/KeyMaps.java
index 48557c2..a7622cb 100644
--- a/src/com/android/calculator2/KeyMaps.java
+++ b/src/com/android/calculator2/KeyMaps.java
@@ -18,8 +18,14 @@ package com.android.calculator2;
import android.content.res.Resources;
import android.content.Context;
+import android.app.Activity;
+import android.util.Log;
import android.view.View;
+import android.widget.Button;
+
import java.text.DecimalFormatSymbols;
+import java.util.HashMap;
+import java.util.Locale;
public class KeyMaps {
// Map key id to corresponding (internationalized) display string
@@ -145,19 +151,31 @@ public class KeyMaps {
}
}
- final static char decimalPt =
+ static char decimalPt =
DecimalFormatSymbols.getInstance().getDecimalSeparator();
+ static char mPiChar;
+
+ static char mFactChar;
+
+ static HashMap<String, Integer> sKeyValForFun;
+ // Key value corresponding to given function name.
+ // We include both localized and English names.
+
+ static String sLocaleForFunMap = "none";
+ // Locale string corresponding to preceding ma and character
+ // constants.
+ // We recompute the map if this is not the current locale.
+
// Return the button id corresponding to the supplied character
// or NO_ID
- // TODO: Should probably also check on characters used as button
- // labels. But those don't really seem to be internationalized.
- public static int keyForChar(char c) {
+ // Called only by UI thread.
+ public static int keyForChar(char c, Activity a) {
+ validateFunMap(a);
if (Character.isDigit(c)) {
int i = Character.digit(c, 10);
return KeyMaps.keyForDigVal(i);
}
- if (c == decimalPt) return R.id.dec_point;
switch (c) {
case '.':
return R.id.dec_point;
@@ -169,12 +187,99 @@ public class KeyMaps {
return R.id.op_mul;
case '/':
return R.id.op_div;
+ // TODO: We have an issue if any of the localized function
+ // names start with 'e' or 'p'. That doesn't currently appear
+ // to be the case. In fact the first letters of the Latin
+ // allphabet ones seem rather predictable.
case 'e':
+ case 'E':
return R.id.const_e;
+ case 'p':
+ case 'P':
+ return R.id.const_pi;
case '^':
return R.id.op_pow;
+ case '!':
+ return R.id.op_fact;
+ case '(':
+ return R.id.lparen;
+ case ')':
+ return R.id.rparen;
default:
+ if (c == decimalPt) return R.id.dec_point;
+ if (c == mPiChar) return R.id.const_pi;
+ // pi is not translated, but it might be typable on
+ // a Greek keyboard, so we check ...
return View.NO_ID;
}
}
+
+ // Add information corresponding to the given button id to
+ // sKeyValForFun.
+ static void addButton(int button_id, Activity a) {
+ Button button = (Button)a.findViewById(button_id);
+ sKeyValForFun.put(button.getText().toString(), button_id);
+ }
+
+ // Ensure that the preceding map and character constants are
+ // initialized and correspond to the current locale.
+ // Called only by a single thread, namely the UI thread.
+ static void validateFunMap(Activity a) {
+ Locale locale = Locale.getDefault();
+ String lname = locale.toString();
+ if (lname != sLocaleForFunMap) {
+ Log.v ("Calculator", "Setting local to: " + lname);
+ sKeyValForFun = new HashMap<String, Integer>();
+ sKeyValForFun.put("sin", R.id.fun_sin);
+ sKeyValForFun.put("cos", R.id.fun_cos);
+ sKeyValForFun.put("tan", R.id.fun_tan);
+ sKeyValForFun.put("arcsin", R.id.fun_arcsin);
+ sKeyValForFun.put("arccos", R.id.fun_arccos);
+ sKeyValForFun.put("arctan", R.id.fun_arctan);
+ sKeyValForFun.put("asin", R.id.fun_arcsin);
+ sKeyValForFun.put("acos", R.id.fun_arccos);
+ sKeyValForFun.put("atan", R.id.fun_arctan);
+ sKeyValForFun.put("ln", R.id.fun_ln);
+ sKeyValForFun.put("log", R.id.fun_log);
+ sKeyValForFun.put("sqrt", R.id.op_sqrt); // special treatment
+ addButton(R.id.fun_sin, a);
+ addButton(R.id.fun_cos, a);
+ addButton(R.id.fun_tan, a);
+ addButton(R.id.fun_arcsin, a);
+ addButton(R.id.fun_arccos, a);
+ addButton(R.id.fun_arctan, a);
+ addButton(R.id.fun_ln, a);
+ addButton(R.id.fun_log, a);
+
+ // Set locale-dependent character "constants"
+ decimalPt =
+ DecimalFormatSymbols.getInstance().getDecimalSeparator();
+ Resources res = a.getResources();
+ mPiChar = mFactChar = 0;
+ String piString = res.getString(R.string.const_pi);
+ if (piString.length() == 1) mPiChar = piString.charAt(0);
+ String factString = res.getString(R.string.op_fact);
+ if (factString.length() == 1) mFactChar = factString.charAt(0);
+
+ sLocaleForFunMap = lname;
+ }
+ }
+
+ // Return function button id for the substring of s starting
+ // at pos and ending with the next "(".
+ // Return NO_ID if there is none.
+ // We check for both standard English names and localized
+ // button labels, though those don't seem to differ much.
+ // Called only by a single thread, namely the UI thread.
+ public static int funForString(String s, int pos, Activity a) {
+ validateFunMap(a);
+ int parenPos = s.indexOf('(', pos);
+ if (parenPos != -1) {
+ String funString = s.substring(pos, parenPos);
+ Integer keyValue = sKeyValForFun.get(funString);
+ if (keyValue == null) return View.NO_ID;
+ return keyValue;
+ }
+ return View.NO_ID;
+ }
}
diff --git a/tests/README.txt b/tests/README.txt
index 40b4f8a..3069de8 100644
--- a/tests/README.txt
+++ b/tests/README.txt
@@ -10,8 +10,8 @@ There are two kinds of tests:
1. A superficial test of calculator functionality through the UI.
This is a resurrected version of a test that appeared in KitKat.
-It's currently mostly a placeholder and some basic infrastructure for
-future tests.
+This is currently only a placeholder for regression tests we shouldn't
+forget; it doesn't yet actually do much of anything.
2. A test of the BoundedRationals library that mostly checks for agreement
with the constructive reals (CR) package. (The BoundedRationals package
diff --git a/tests/src/com/android/calculator2/BRTest.java b/tests/src/com/android/calculator2/BRTest.java
index bd7070a..1cd56a5 100644
--- a/tests/src/com/android/calculator2/BRTest.java
+++ b/tests/src/com/android/calculator2/BRTest.java
@@ -119,6 +119,9 @@ public class BRTest extends TestCase {
}
public void testBR() {
+ BoundedRational b = new BoundedRational(4,-6);
+ check(b.toString().equals("4/-6"), "toString(4/-6)");
+ check(b.toNiceString().equals("-2/3"),"toNiceString(4/-6)");
checkEq(BR_0, CR.valueOf(0), "0");
checkEq(BR_390, CR.valueOf(390), "390");
checkEq(BR_15, CR.valueOf(15), "15");
diff --git a/tests/src/com/android/calculator2/CalculatorHitSomeButtons.java b/tests/src/com/android/calculator2/CalculatorHitSomeButtons.java
index d26a5cb..a075a64 100644
--- a/tests/src/com/android/calculator2/CalculatorHitSomeButtons.java
+++ b/tests/src/com/android/calculator2/CalculatorHitSomeButtons.java
@@ -26,9 +26,9 @@ import android.test.suitebuilder.annotation.LargeTest;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
-import android.widget.EditText;
import android.widget.Button;
import android.widget.LinearLayout;
+import android.widget.TextView;
import android.graphics.Rect;
import android.test.TouchUtils;
@@ -64,7 +64,7 @@ public class CalculatorHitSomeButtons extends ActivityInstrumentationTestCase <C
super.tearDown();
}
-/*
+
@LargeTest
public void testPressSomeKeys() {
Log.v(TAG, "Pressing some keys!");
@@ -83,7 +83,7 @@ public class CalculatorHitSomeButtons extends ActivityInstrumentationTestCase <C
checkDisplay("23");
}
-*/
+
@LargeTest
public void testTapSomeButtons() {
@@ -102,6 +102,7 @@ public class CalculatorHitSomeButtons extends ActivityInstrumentationTestCase <C
tap(R.id.digit_7);
tap(R.id.op_div);
tap(R.id.digit_3);
+ tap(R.id.dec_point);
tap(R.id.eq);
checkDisplay("189");
@@ -116,6 +117,24 @@ public class CalculatorHitSomeButtons extends ActivityInstrumentationTestCase <C
// Careful: the first digit in the expected value is \u2212, not "-" (a hyphen)
checkDisplay(mActivity.getString(R.string.op_sub) + "600");
+
+ tap(R.id.dec_point);
+ tap(R.id.digit_5);
+ tap(R.id.op_add);
+ tap(R.id.dec_point);
+ tap(R.id.digit_5);
+ tap(R.id.eq);
+ checkDisplay("1");
+
+ tap(R.id.digit_5);
+ tap(R.id.op_div);
+ tap(R.id.digit_3);
+ tap(R.id.dec_point);
+ tap(R.id.digit_5);
+ tap(R.id.op_mul);
+ tap(R.id.digit_7);
+ tap(R.id.eq);
+ checkDisplay("10");
}
// helper functions
@@ -123,39 +142,35 @@ public class CalculatorHitSomeButtons extends ActivityInstrumentationTestCase <C
mInst.sendKeyDownUpSync(keycode);
}
- private boolean tap(int id) {
+ private void tap(int id) {
View view = mActivity.findViewById(id);
- if(view != null) {
- TouchUtils.clickView(this, view);
- return true;
- }
- return false;
+ assertNotNull(view);
+ TouchUtils.clickView(this, view);
}
private void checkDisplay(final String s) {
- mInst.waitForIdle(new Runnable () {
- @Override
- public void run() {
- try {
- Thread.sleep(20); // Wait for background computation
- } catch(InterruptedException ignored) {
- fail("Unexpected interrupt");
+ /*
+ FIXME: This doesn't yet work.
+ try {
+ Thread.sleep(20);
+ runTestOnUiThread(new Runnable () {
+ @Override
+ public void run() {
+ Log.v(TAG, "Display:" + displayVal());
+ assertEquals(s, displayVal());
}
- mInst.waitForIdle(new Runnable () {
- @Override
- public void run() {
- assertEquals(displayVal(), s);
- }
- });
- }
- });
+ });
+ } catch (Throwable e) {
+ fail("unexpected exception" + e);
+ }
+ */
}
private String displayVal() {
CalculatorResult display = (CalculatorResult) mActivity.findViewById(R.id.result);
assertNotNull(display);
- EditText box = (EditText) display;
+ TextView box = (TextView) display;
assertNotNull(box);
return box.getText().toString();