1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *   http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.calculator2;
18 
19 import android.annotation.TargetApi;
20 import android.content.ClipData;
21 import android.content.ClipboardManager;
22 import android.content.Context;
23 import android.content.res.TypedArray;
24 import android.graphics.Rect;
25 import android.os.Build;
26 import android.text.Layout;
27 import android.text.TextPaint;
28 import android.text.TextUtils;
29 import android.util.AttributeSet;
30 import android.util.Log;
31 import android.util.TypedValue;
32 import android.view.ActionMode;
33 import android.view.ContextMenu;
34 import android.view.Menu;
35 import android.view.MenuInflater;
36 import android.view.MenuItem;
37 import android.view.View;
38 import android.widget.TextView;
39 
40 /**
41  * TextView adapted for displaying the formula and allowing pasting.
42  */
43 public class CalculatorFormula extends AlignedTextView implements MenuItem.OnMenuItemClickListener,
44         ClipboardManager.OnPrimaryClipChangedListener {
45 
46     public static final String TAG_ACTION_MODE = "ACTION_MODE";
47 
48     // Temporary paint for use in layout methods.
49     private final TextPaint mTempPaint = new TextPaint();
50 
51     private final float mMaximumTextSize;
52     private final float mMinimumTextSize;
53     private final float mStepTextSize;
54 
55     private final ClipboardManager mClipboardManager;
56 
57     private int mWidthConstraint = -1;
58     private ActionMode mActionMode;
59     private ActionMode.Callback mPasteActionModeCallback;
60     private ContextMenu mContextMenu;
61     private OnTextSizeChangeListener mOnTextSizeChangeListener;
62     private OnFormulaContextMenuClickListener mOnContextMenuClickListener;
63     private Calculator.OnDisplayMemoryOperationsListener mOnDisplayMemoryOperationsListener;
64 
CalculatorFormula(Context context)65     public CalculatorFormula(Context context) {
66         this(context, null /* attrs */);
67     }
68 
CalculatorFormula(Context context, AttributeSet attrs)69     public CalculatorFormula(Context context, AttributeSet attrs) {
70         this(context, attrs, 0 /* defStyleAttr */);
71     }
72 
CalculatorFormula(Context context, AttributeSet attrs, int defStyleAttr)73     public CalculatorFormula(Context context, AttributeSet attrs, int defStyleAttr) {
74         super(context, attrs, defStyleAttr);
75 
76         mClipboardManager = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
77 
78         final TypedArray a = context.obtainStyledAttributes(
79                 attrs, R.styleable.CalculatorFormula, defStyleAttr, 0);
80         mMaximumTextSize = a.getDimension(
81                 R.styleable.CalculatorFormula_maxTextSize, getTextSize());
82         mMinimumTextSize = a.getDimension(
83                 R.styleable.CalculatorFormula_minTextSize, getTextSize());
84         mStepTextSize = a.getDimension(R.styleable.CalculatorFormula_stepTextSize,
85                 (mMaximumTextSize - mMinimumTextSize) / 3);
86         a.recycle();
87 
88         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
89             setupActionMode();
90         } else {
91             setupContextMenu();
92         }
93     }
94 
95     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)96     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
97         if (!isLaidOut()) {
98             // Prevent shrinking/resizing with our variable textSize.
99             setTextSizeInternal(TypedValue.COMPLEX_UNIT_PX, mMaximumTextSize,
100                     false /* notifyListener */);
101             setMinimumHeight(getLineHeight() + getCompoundPaddingBottom()
102                     + getCompoundPaddingTop());
103         }
104 
105         // Ensure we are at least as big as our parent.
106         final int width = MeasureSpec.getSize(widthMeasureSpec);
107         if (getMinimumWidth() != width) {
108             setMinimumWidth(width);
109         }
110 
111         // Re-calculate our textSize based on new width.
112         mWidthConstraint = MeasureSpec.getSize(widthMeasureSpec)
113                 - getPaddingLeft() - getPaddingRight();
114         final float textSize = getVariableTextSize(getText());
115         if (getTextSize() != textSize) {
116             setTextSizeInternal(TypedValue.COMPLEX_UNIT_PX, textSize, false /* notifyListener */);
117         }
118 
119         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
120     }
121 
122     @Override
onAttachedToWindow()123     protected void onAttachedToWindow() {
124         super.onAttachedToWindow();
125 
126         mClipboardManager.addPrimaryClipChangedListener(this);
127         onPrimaryClipChanged();
128     }
129 
130     @Override
onDetachedFromWindow()131     protected void onDetachedFromWindow() {
132         super.onDetachedFromWindow();
133 
134         mClipboardManager.removePrimaryClipChangedListener(this);
135     }
136 
137     @Override
onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter)138     protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
139         super.onTextChanged(text, start, lengthBefore, lengthAfter);
140 
141         setTextSize(TypedValue.COMPLEX_UNIT_PX, getVariableTextSize(text.toString()));
142     }
143 
setTextSizeInternal(int unit, float size, boolean notifyListener)144     private void setTextSizeInternal(int unit, float size, boolean notifyListener) {
145         final float oldTextSize = getTextSize();
146         super.setTextSize(unit, size);
147         if (notifyListener && mOnTextSizeChangeListener != null && getTextSize() != oldTextSize) {
148             mOnTextSizeChangeListener.onTextSizeChanged(this, oldTextSize);
149         }
150     }
151 
152     @Override
setTextSize(int unit, float size)153     public void setTextSize(int unit, float size) {
154         setTextSizeInternal(unit, size, true);
155     }
156 
getMinimumTextSize()157     public float getMinimumTextSize() {
158         return mMinimumTextSize;
159     }
160 
getMaximumTextSize()161     public float getMaximumTextSize() {
162         return mMaximumTextSize;
163     }
164 
getVariableTextSize(CharSequence text)165     public float getVariableTextSize(CharSequence text) {
166         if (mWidthConstraint < 0 || mMaximumTextSize <= mMinimumTextSize) {
167             // Not measured, bail early.
168             return getTextSize();
169         }
170 
171         // Capture current paint state.
172         mTempPaint.set(getPaint());
173 
174         // Step through increasing text sizes until the text would no longer fit.
175         float lastFitTextSize = mMinimumTextSize;
176         while (lastFitTextSize < mMaximumTextSize) {
177             mTempPaint.setTextSize(Math.min(lastFitTextSize + mStepTextSize, mMaximumTextSize));
178             if (Layout.getDesiredWidth(text, mTempPaint) > mWidthConstraint) {
179                 break;
180             }
181             lastFitTextSize = mTempPaint.getTextSize();
182         }
183 
184         return lastFitTextSize;
185     }
186 
187     /**
188      * Functionally equivalent to setText(), but explicitly announce changes.
189      * If the new text is an extension of the old one, announce the addition.
190      * Otherwise, e.g. after deletion, announce the entire new text.
191      */
changeTextTo(CharSequence newText)192     public void changeTextTo(CharSequence newText) {
193         final CharSequence oldText = getText();
194         final char separator = KeyMaps.translateResult(",").charAt(0);
195         final CharSequence added = StringUtils.getExtensionIgnoring(newText, oldText, separator);
196         if (added != null) {
197             if (added.length() == 1) {
198                 // The algorithm for pronouncing a single character doesn't seem
199                 // to respect our hints.  Don't give it the choice.
200                 final char c = added.charAt(0);
201                 final int id = KeyMaps.keyForChar(c);
202                 final String descr = KeyMaps.toDescriptiveString(getContext(), id);
203                 if (descr != null) {
204                     announceForAccessibility(descr);
205                 } else {
206                     announceForAccessibility(String.valueOf(c));
207                 }
208             } else if (added.length() != 0) {
209                 announceForAccessibility(added);
210             }
211         } else {
212             announceForAccessibility(newText);
213         }
214         setText(newText, BufferType.SPANNABLE);
215     }
216 
stopActionModeOrContextMenu()217     public boolean stopActionModeOrContextMenu() {
218         if (mActionMode != null) {
219             mActionMode.finish();
220             return true;
221         }
222         if (mContextMenu != null) {
223             mContextMenu.close();
224             return true;
225         }
226         return false;
227     }
228 
setOnTextSizeChangeListener(OnTextSizeChangeListener listener)229     public void setOnTextSizeChangeListener(OnTextSizeChangeListener listener) {
230         mOnTextSizeChangeListener = listener;
231     }
232 
setOnContextMenuClickListener(OnFormulaContextMenuClickListener listener)233     public void setOnContextMenuClickListener(OnFormulaContextMenuClickListener listener) {
234         mOnContextMenuClickListener = listener;
235     }
236 
setOnDisplayMemoryOperationsListener( Calculator.OnDisplayMemoryOperationsListener listener)237     public void setOnDisplayMemoryOperationsListener(
238             Calculator.OnDisplayMemoryOperationsListener listener) {
239         mOnDisplayMemoryOperationsListener = listener;
240     }
241 
242     /**
243      * Use ActionMode for paste support on M and higher.
244      */
245     @TargetApi(Build.VERSION_CODES.M)
setupActionMode()246     private void setupActionMode() {
247         mPasteActionModeCallback = new ActionMode.Callback2() {
248 
249             @Override
250             public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
251                 if (onMenuItemClick(item)) {
252                     mode.finish();
253                     return true;
254                 } else {
255                     return false;
256                 }
257             }
258 
259             @Override
260             public boolean onCreateActionMode(ActionMode mode, Menu menu) {
261                 mode.setTag(TAG_ACTION_MODE);
262                 final MenuInflater inflater = mode.getMenuInflater();
263                 return createContextMenu(inflater, menu);
264             }
265 
266             @Override
267             public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
268                 return false;
269             }
270 
271             @Override
272             public void onDestroyActionMode(ActionMode mode) {
273                 mActionMode = null;
274             }
275 
276             @Override
277             public void onGetContentRect(ActionMode mode, View view, Rect outRect) {
278                 super.onGetContentRect(mode, view, outRect);
279                 outRect.top += getTotalPaddingTop();
280                 outRect.right -= getTotalPaddingRight();
281                 outRect.bottom -= getTotalPaddingBottom();
282                 // Encourage menu positioning over the rightmost 10% of the screen.
283                 outRect.left = (int) (outRect.right * 0.9f);
284             }
285         };
286         setOnLongClickListener(new View.OnLongClickListener() {
287             @Override
288             public boolean onLongClick(View v) {
289                 mActionMode = startActionMode(mPasteActionModeCallback, ActionMode.TYPE_FLOATING);
290                 return true;
291             }
292         });
293     }
294 
295     /**
296      * Use ContextMenu for paste support on L and lower.
297      */
setupContextMenu()298     private void setupContextMenu() {
299         setOnCreateContextMenuListener(new OnCreateContextMenuListener() {
300             @Override
301             public void onCreateContextMenu(ContextMenu contextMenu, View view,
302                     ContextMenu.ContextMenuInfo contextMenuInfo) {
303                 final MenuInflater inflater = new MenuInflater(getContext());
304                 createContextMenu(inflater, contextMenu);
305                 mContextMenu = contextMenu;
306                 for (int i = 0; i < contextMenu.size(); i++) {
307                     contextMenu.getItem(i).setOnMenuItemClickListener(CalculatorFormula.this);
308                 }
309             }
310         });
311         setOnLongClickListener(new View.OnLongClickListener() {
312             @Override
313             public boolean onLongClick(View v) {
314                 return showContextMenu();
315             }
316         });
317     }
318 
createContextMenu(MenuInflater inflater, Menu menu)319     private boolean createContextMenu(MenuInflater inflater, Menu menu) {
320         final boolean isPasteEnabled = isPasteEnabled();
321         final boolean isMemoryEnabled = isMemoryEnabled();
322         if (!isPasteEnabled && !isMemoryEnabled) {
323             return false;
324         }
325 
326         bringPointIntoView(length());
327         inflater.inflate(R.menu.menu_formula, menu);
328         final MenuItem pasteItem = menu.findItem(R.id.menu_paste);
329         final MenuItem memoryRecallItem = menu.findItem(R.id.memory_recall);
330         pasteItem.setEnabled(isPasteEnabled);
331         memoryRecallItem.setEnabled(isMemoryEnabled);
332         return true;
333     }
334 
paste()335     private void paste() {
336         final ClipData primaryClip = mClipboardManager.getPrimaryClip();
337         if (primaryClip != null && mOnContextMenuClickListener != null) {
338             mOnContextMenuClickListener.onPaste(primaryClip);
339         }
340     }
341 
342     @Override
onMenuItemClick(MenuItem item)343     public boolean onMenuItemClick(MenuItem item) {
344         switch (item.getItemId()) {
345             case R.id.memory_recall:
346                 mOnContextMenuClickListener.onMemoryRecall();
347                 return true;
348             case R.id.menu_paste:
349                 paste();
350                 return true;
351             default:
352                 return false;
353         }
354     }
355 
356     @Override
onPrimaryClipChanged()357     public void onPrimaryClipChanged() {
358         setLongClickable(isPasteEnabled() || isMemoryEnabled());
359     }
360 
onMemoryStateChanged()361     public void onMemoryStateChanged() {
362         setLongClickable(isPasteEnabled() || isMemoryEnabled());
363     }
364 
isMemoryEnabled()365     private boolean isMemoryEnabled() {
366         return mOnDisplayMemoryOperationsListener != null
367                 && mOnDisplayMemoryOperationsListener.shouldDisplayMemory();
368     }
369 
isPasteEnabled()370     private boolean isPasteEnabled() {
371         final ClipData clip = mClipboardManager.getPrimaryClip();
372         if (clip == null || clip.getItemCount() == 0) {
373             return false;
374         }
375         CharSequence clipText = null;
376         try {
377             clipText = clip.getItemAt(0).coerceToText(getContext());
378         } catch (Exception e) {
379             Log.i("Calculator", "Error reading clipboard:", e);
380         }
381         return !TextUtils.isEmpty(clipText);
382     }
383 
384     public interface OnTextSizeChangeListener {
onTextSizeChanged(TextView textView, float oldSize)385         void onTextSizeChanged(TextView textView, float oldSize);
386     }
387 
388     public interface OnFormulaContextMenuClickListener {
onPaste(ClipData clip)389         boolean onPaste(ClipData clip);
onMemoryRecall()390         void onMemoryRecall();
391     }
392 }
393