blob: 2911df8dede03edd414da36d9730b4863506736a [file] [log] [blame]
Dmitri Plotnikovde3eec22011-01-17 18:23:37 -08001/*
Justin Klaassen44595162015-05-28 17:55:20 -07002 * Copyright (C) 2015 The Android Open Source Project
Dmitri Plotnikovde3eec22011-01-17 18:23:37 -08003 *
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 *
Justin Klaassen4b3af052014-05-27 17:53:10 -07008 * http://www.apache.org/licenses/LICENSE-2.0
Dmitri Plotnikovde3eec22011-01-17 18:23:37 -08009 *
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
17package com.android.calculator2;
18
Chenjie Yu3937b652016-06-01 23:14:26 -070019import android.annotation.TargetApi;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070020import android.content.ClipData;
Justin Klaassen44595162015-05-28 17:55:20 -070021import android.content.ClipboardManager;
Dmitri Plotnikovde3eec22011-01-17 18:23:37 -080022import android.content.Context;
Justin Klaassen4b3af052014-05-27 17:53:10 -070023import android.content.res.TypedArray;
Hans Boehm7f83e362015-06-10 15:41:04 -070024import android.graphics.Rect;
Chenjie Yu3937b652016-06-01 23:14:26 -070025import android.os.Build;
Justin Klaassenfc5ac822015-06-18 13:15:17 -070026import android.text.Layout;
Justin Klaassen4b3af052014-05-27 17:53:10 -070027import android.text.TextPaint;
Christine Franksafe28bb2016-07-29 17:24:52 -070028import android.text.TextUtils;
Dmitri Plotnikovde3eec22011-01-17 18:23:37 -080029import android.util.AttributeSet;
Annie Chin7d50adf2016-10-27 15:55:38 -070030import android.util.Log;
Hongwei Wang245925e2014-05-11 14:38:47 -070031import android.util.TypedValue;
Gilles Debunnef57b8b42011-01-27 10:54:07 -080032import android.view.ActionMode;
Chenjie Yu3937b652016-06-01 23:14:26 -070033import android.view.ContextMenu;
Gilles Debunnef57b8b42011-01-27 10:54:07 -080034import android.view.Menu;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070035import android.view.MenuInflater;
Gilles Debunnef57b8b42011-01-27 10:54:07 -080036import android.view.MenuItem;
Hans Boehm76b78152015-04-17 10:50:35 -070037import android.view.View;
Justin Klaassenfed941a2014-06-09 18:42:40 +010038import android.widget.TextView;
Dmitri Plotnikovde3eec22011-01-17 18:23:37 -080039
Hans Boehm84614952014-11-25 18:46:17 -080040/**
Christine Franks7452d3a2016-10-27 13:41:18 -070041 * TextView adapted for displaying the formula and allowing pasting.
Hans Boehm84614952014-11-25 18:46:17 -080042 */
Christine Franks7452d3a2016-10-27 13:41:18 -070043public class CalculatorFormula extends AlignedTextView implements MenuItem.OnMenuItemClickListener,
Christine Franksafe28bb2016-07-29 17:24:52 -070044 ClipboardManager.OnPrimaryClipChangedListener {
Alan Viverette461992d2014-03-07 13:29:56 -080045
Annie Chine918fd22016-03-09 11:07:54 -080046 public static final String TAG_ACTION_MODE = "ACTION_MODE";
47
Justin Klaassenfc5ac822015-06-18 13:15:17 -070048 // Temporary paint for use in layout methods.
49 private final TextPaint mTempPaint = new TextPaint();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070050
Justin Klaassen4b3af052014-05-27 17:53:10 -070051 private final float mMaximumTextSize;
52 private final float mMinimumTextSize;
53 private final float mStepTextSize;
54
Christine Franksafe28bb2016-07-29 17:24:52 -070055 private final ClipboardManager mClipboardManager;
56
Justin Klaassen4b3af052014-05-27 17:53:10 -070057 private int mWidthConstraint = -1;
Christine Frankscbc51fa2017-01-04 21:00:36 -080058 private ActionMode mActionMode;
Chenjie Yu3937b652016-06-01 23:14:26 -070059 private ActionMode.Callback mPasteActionModeCallback;
Christine Frankscbc51fa2017-01-04 21:00:36 -080060 private ContextMenu mContextMenu;
Justin Klaassenfed941a2014-06-09 18:42:40 +010061 private OnTextSizeChangeListener mOnTextSizeChangeListener;
Christine Franks1d99be12016-11-14 14:00:36 -080062 private OnFormulaContextMenuClickListener mOnContextMenuClickListener;
63 private Calculator.OnDisplayMemoryOperationsListener mOnDisplayMemoryOperationsListener;
Justin Klaassen4b3af052014-05-27 17:53:10 -070064
Christine Franks7452d3a2016-10-27 13:41:18 -070065 public CalculatorFormula(Context context) {
Justin Klaassenfc5ac822015-06-18 13:15:17 -070066 this(context, null /* attrs */);
Justin Klaassen4b3af052014-05-27 17:53:10 -070067 }
68
Christine Franks7452d3a2016-10-27 13:41:18 -070069 public CalculatorFormula(Context context, AttributeSet attrs) {
Justin Klaassenfc5ac822015-06-18 13:15:17 -070070 this(context, attrs, 0 /* defStyleAttr */);
Justin Klaassen4b3af052014-05-27 17:53:10 -070071 }
72
Christine Franks7452d3a2016-10-27 13:41:18 -070073 public CalculatorFormula(Context context, AttributeSet attrs, int defStyleAttr) {
Justin Klaassenfc5ac822015-06-18 13:15:17 -070074 super(context, attrs, defStyleAttr);
Justin Klaassen4b3af052014-05-27 17:53:10 -070075
Christine Franksafe28bb2016-07-29 17:24:52 -070076 mClipboardManager = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
77
Justin Klaassen4b3af052014-05-27 17:53:10 -070078 final TypedArray a = context.obtainStyledAttributes(
Christine Franks7452d3a2016-10-27 13:41:18 -070079 attrs, R.styleable.CalculatorFormula, defStyleAttr, 0);
Justin Klaassen4b3af052014-05-27 17:53:10 -070080 mMaximumTextSize = a.getDimension(
Christine Franks7452d3a2016-10-27 13:41:18 -070081 R.styleable.CalculatorFormula_maxTextSize, getTextSize());
Justin Klaassen4b3af052014-05-27 17:53:10 -070082 mMinimumTextSize = a.getDimension(
Christine Franks7452d3a2016-10-27 13:41:18 -070083 R.styleable.CalculatorFormula_minTextSize, getTextSize());
84 mStepTextSize = a.getDimension(R.styleable.CalculatorFormula_stepTextSize,
Justin Klaassen4b3af052014-05-27 17:53:10 -070085 (mMaximumTextSize - mMinimumTextSize) / 3);
Justin Klaassen4b3af052014-05-27 17:53:10 -070086 a.recycle();
87
Chenjie Yu3937b652016-06-01 23:14:26 -070088 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
89 setupActionMode();
90 } else {
91 setupContextMenu();
92 }
Hans Boehm76b78152015-04-17 10:50:35 -070093 }
Justin Klaassen4b3af052014-05-27 17:53:10 -070094
95 @Override
96 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Justin Klaassen0ace4eb2016-02-05 11:38:12 -080097 if (!isLaidOut()) {
98 // Prevent shrinking/resizing with our variable textSize.
99 setTextSizeInternal(TypedValue.COMPLEX_UNIT_PX, mMaximumTextSize,
100 false /* notifyListener */);
Justin Klaassenf3076c82016-06-03 13:18:55 -0700101 setMinimumHeight(getLineHeight() + getCompoundPaddingBottom()
Justin Klaassen0ace4eb2016-02-05 11:38:12 -0800102 + getCompoundPaddingTop());
103 }
104
Justin Klaassenf3076c82016-06-03 13:18:55 -0700105 // 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
Justin Klaassenfc5ac822015-06-18 13:15:17 -0700111 // Re-calculate our textSize based on new width.
Justin Klaassen0ace4eb2016-02-05 11:38:12 -0800112 mWidthConstraint = MeasureSpec.getSize(widthMeasureSpec)
Justin Klaassen44595162015-05-28 17:55:20 -0700113 - getPaddingLeft() - getPaddingRight();
Justin Klaassen0ace4eb2016-02-05 11:38:12 -0800114 final float textSize = getVariableTextSize(getText());
115 if (getTextSize() != textSize) {
116 setTextSizeInternal(TypedValue.COMPLEX_UNIT_PX, textSize, false /* notifyListener */);
Justin Klaassenfc5ac822015-06-18 13:15:17 -0700117 }
118
119 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700120 }
121
122 @Override
Christine Franksafe28bb2016-07-29 17:24:52 -0700123 protected void onAttachedToWindow() {
124 super.onAttachedToWindow();
125
126 mClipboardManager.addPrimaryClipChangedListener(this);
127 onPrimaryClipChanged();
128 }
129
130 @Override
131 protected void onDetachedFromWindow() {
132 super.onDetachedFromWindow();
133
134 mClipboardManager.removePrimaryClipChangedListener(this);
135 }
136
137 @Override
Justin Klaassen4b3af052014-05-27 17:53:10 -0700138 protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
139 super.onTextChanged(text, start, lengthBefore, lengthAfter);
Justin Klaassenbfc4e4d2014-08-27 10:56:49 -0700140
Justin Klaassen4b3af052014-05-27 17:53:10 -0700141 setTextSize(TypedValue.COMPLEX_UNIT_PX, getVariableTextSize(text.toString()));
142 }
143
Hans Boehm11e37a82015-10-01 14:41:37 -0700144 private void setTextSizeInternal(int unit, float size, boolean notifyListener) {
Justin Klaassenfed941a2014-06-09 18:42:40 +0100145 final float oldTextSize = getTextSize();
146 super.setTextSize(unit, size);
Hans Boehm11e37a82015-10-01 14:41:37 -0700147 if (notifyListener && mOnTextSizeChangeListener != null && getTextSize() != oldTextSize) {
Justin Klaassenfed941a2014-06-09 18:42:40 +0100148 mOnTextSizeChangeListener.onTextSizeChanged(this, oldTextSize);
149 }
150 }
151
Hans Boehm11e37a82015-10-01 14:41:37 -0700152 @Override
153 public void setTextSize(int unit, float size) {
154 setTextSizeInternal(unit, size, true);
155 }
156
Justin Klaassen44595162015-05-28 17:55:20 -0700157 public float getMinimumTextSize() {
158 return mMinimumTextSize;
159 }
160
161 public float getMaximumTextSize() {
162 return mMaximumTextSize;
163 }
164
Justin Klaassenfc5ac822015-06-18 13:15:17 -0700165 public float getVariableTextSize(CharSequence text) {
Justin Klaassen4b3af052014-05-27 17:53:10 -0700166 if (mWidthConstraint < 0 || mMaximumTextSize <= mMinimumTextSize) {
167 // Not measured, bail early.
168 return getTextSize();
169 }
170
Justin Klaassen2be4fdb2014-08-06 19:54:09 -0700171 // Capture current paint state.
172 mTempPaint.set(getPaint());
173
174 // Step through increasing text sizes until the text would no longer fit.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700175 float lastFitTextSize = mMinimumTextSize;
176 while (lastFitTextSize < mMaximumTextSize) {
Justin Klaassenfc5ac822015-06-18 13:15:17 -0700177 mTempPaint.setTextSize(Math.min(lastFitTextSize + mStepTextSize, mMaximumTextSize));
178 if (Layout.getDesiredWidth(text, mTempPaint) > mWidthConstraint) {
Justin Klaassen4b3af052014-05-27 17:53:10 -0700179 break;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700180 }
Justin Klaassenfc5ac822015-06-18 13:15:17 -0700181 lastFitTextSize = mTempPaint.getTextSize();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700182 }
183
184 return lastFitTextSize;
185 }
186
Hans Boehmccc55662015-07-07 14:16:59 -0700187 /**
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 */
192 public void changeTextTo(CharSequence newText) {
Hans Boehm8a4f81c2015-07-09 10:41:25 -0700193 final CharSequence oldText = getText();
Hans Boehm24c91ed2016-06-30 18:53:44 -0700194 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) {
Hans Boehm8a4f81c2015-07-09 10:41:25 -0700198 // The algorithm for pronouncing a single character doesn't seem
199 // to respect our hints. Don't give it the choice.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700200 final char c = added.charAt(0);
Hans Boehm8a4f81c2015-07-09 10:41:25 -0700201 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 }
Hans Boehm24c91ed2016-06-30 18:53:44 -0700208 } else if (added.length() != 0) {
209 announceForAccessibility(added);
Hans Boehmccc55662015-07-07 14:16:59 -0700210 }
211 } else {
212 announceForAccessibility(newText);
213 }
Justin Klaassend1831412016-07-19 21:59:10 -0700214 setText(newText, BufferType.SPANNABLE);
Hans Boehmccc55662015-07-07 14:16:59 -0700215 }
216
Chenjie Yu3937b652016-06-01 23:14:26 -0700217 public boolean stopActionModeOrContextMenu() {
Hans Boehm1176f232015-05-11 16:26:03 -0700218 if (mActionMode != null) {
219 mActionMode.finish();
220 return true;
221 }
Chenjie Yu3937b652016-06-01 23:14:26 -0700222 if (mContextMenu != null) {
223 mContextMenu.close();
224 return true;
225 }
Hans Boehm1176f232015-05-11 16:26:03 -0700226 return false;
227 }
228
Justin Klaassenfc5ac822015-06-18 13:15:17 -0700229 public void setOnTextSizeChangeListener(OnTextSizeChangeListener listener) {
230 mOnTextSizeChangeListener = listener;
231 }
232
Christine Franks1d99be12016-11-14 14:00:36 -0800233 public void setOnContextMenuClickListener(OnFormulaContextMenuClickListener listener) {
234 mOnContextMenuClickListener = listener;
235 }
236
237 public void setOnDisplayMemoryOperationsListener(
238 Calculator.OnDisplayMemoryOperationsListener listener) {
239 mOnDisplayMemoryOperationsListener = listener;
Justin Klaassenfc5ac822015-06-18 13:15:17 -0700240 }
241
Chenjie Yu3937b652016-06-01 23:14:26 -0700242 /**
243 * Use ActionMode for paste support on M and higher.
244 */
245 @TargetApi(Build.VERSION_CODES.M)
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();
Christine Franks1d99be12016-11-14 14:00:36 -0800263 return createContextMenu(inflater, menu);
Chenjie Yu3937b652016-06-01 23:14:26 -0700264 }
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();
Christine Franksbe739a12016-10-20 12:45:55 -0700282 // Encourage menu positioning over the rightmost 10% of the screen.
283 outRect.left = (int) (outRect.right * 0.9f);
Chenjie Yu3937b652016-06-01 23:14:26 -0700284 }
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 */
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());
Christine Franks1d99be12016-11-14 14:00:36 -0800304 createContextMenu(inflater, contextMenu);
Chenjie Yu3937b652016-06-01 23:14:26 -0700305 mContextMenu = contextMenu;
Hans Boehm9c160b42016-12-02 11:55:12 -0800306 for (int i = 0; i < contextMenu.size(); i++) {
Christine Franks7452d3a2016-10-27 13:41:18 -0700307 contextMenu.getItem(i).setOnMenuItemClickListener(CalculatorFormula.this);
Chenjie Yu3937b652016-06-01 23:14:26 -0700308 }
309 }
310 });
311 setOnLongClickListener(new View.OnLongClickListener() {
312 @Override
313 public boolean onLongClick(View v) {
314 return showContextMenu();
315 }
316 });
317 }
318
Christine Franks1d99be12016-11-14 14:00:36 -0800319 private boolean createContextMenu(MenuInflater inflater, Menu menu) {
Christine Franks61c0ed92016-12-08 15:03:53 -0800320 final boolean isPasteEnabled = isPasteEnabled();
Christine Franks1d99be12016-11-14 14:00:36 -0800321 final boolean isMemoryEnabled = isMemoryEnabled();
322 if (!isPasteEnabled && !isMemoryEnabled) {
323 return false;
Chenjie Yu3937b652016-06-01 23:14:26 -0700324 }
Christine Franks1d99be12016-11-14 14:00:36 -0800325
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;
Chenjie Yu3937b652016-06-01 23:14:26 -0700333 }
334
Justin Klaassenfc5ac822015-06-18 13:15:17 -0700335 private void paste() {
Christine Franks61c0ed92016-12-08 15:03:53 -0800336 final ClipData primaryClip = mClipboardManager.getPrimaryClip();
Christine Franks1d99be12016-11-14 14:00:36 -0800337 if (primaryClip != null && mOnContextMenuClickListener != null) {
338 mOnContextMenuClickListener.onPaste(primaryClip);
Justin Klaassenfc5ac822015-06-18 13:15:17 -0700339 }
340 }
341
Chenjie Yu3937b652016-06-01 23:14:26 -0700342 @Override
343 public boolean onMenuItemClick(MenuItem item) {
Christine Franks1d99be12016-11-14 14:00:36 -0800344 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;
Chenjie Yu3937b652016-06-01 23:14:26 -0700353 }
Chenjie Yu3937b652016-06-01 23:14:26 -0700354 }
355
Christine Franksafe28bb2016-07-29 17:24:52 -0700356 @Override
357 public void onPrimaryClipChanged() {
Christine Franks61c0ed92016-12-08 15:03:53 -0800358 setLongClickable(isPasteEnabled() || isMemoryEnabled());
359 }
360
361 public void onMemoryStateChanged() {
362 setLongClickable(isPasteEnabled() || isMemoryEnabled());
363 }
364
365 private boolean isMemoryEnabled() {
Christine Frankscbc51fa2017-01-04 21:00:36 -0800366 return mOnDisplayMemoryOperationsListener != null
Christine Franks61c0ed92016-12-08 15:03:53 -0800367 && mOnDisplayMemoryOperationsListener.shouldDisplayMemory();
368 }
369
370 private boolean isPasteEnabled() {
Christine Franksafe28bb2016-07-29 17:24:52 -0700371 final ClipData clip = mClipboardManager.getPrimaryClip();
372 if (clip == null || clip.getItemCount() == 0) {
Christine Franks61c0ed92016-12-08 15:03:53 -0800373 return false;
Christine Franksafe28bb2016-07-29 17:24:52 -0700374 }
Annie Chin7d50adf2016-10-27 15:55:38 -0700375 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 }
Christine Franks61c0ed92016-12-08 15:03:53 -0800381 return !TextUtils.isEmpty(clipText);
Christine Franksafe28bb2016-07-29 17:24:52 -0700382 }
383
Justin Klaassenfed941a2014-06-09 18:42:40 +0100384 public interface OnTextSizeChangeListener {
385 void onTextSizeChanged(TextView textView, float oldSize);
386 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700387
Christine Franks1d99be12016-11-14 14:00:36 -0800388 public interface OnFormulaContextMenuClickListener {
Justin Klaassenfc5ac822015-06-18 13:15:17 -0700389 boolean onPaste(ClipData clip);
Christine Franks1d99be12016-11-14 14:00:36 -0800390 void onMemoryRecall();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700391 }
Dmitri Plotnikovde3eec22011-01-17 18:23:37 -0800392}