blob: 210372cd7873bd9293dab601da6ec97008506b77 [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;
Justin Klaassenfc5ac822015-06-18 13:15:17 -070058 private ActionMode mActionMode;
Chenjie Yu3937b652016-06-01 23:14:26 -070059 private ActionMode.Callback mPasteActionModeCallback;
60 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 private static boolean startsWith(CharSequence whole, CharSequence prefix) {
188 int wholeLen = whole.length();
189 int prefixLen = prefix.length();
190 if (prefixLen > wholeLen) {
191 return false;
192 }
193 for (int i = 0; i < prefixLen; ++i) {
194 if (prefix.charAt(i) != whole.charAt(i)) {
195 return false;
196 }
197 }
198 return true;
199 }
200
201 /**
202 * Functionally equivalent to setText(), but explicitly announce changes.
203 * If the new text is an extension of the old one, announce the addition.
204 * Otherwise, e.g. after deletion, announce the entire new text.
205 */
206 public void changeTextTo(CharSequence newText) {
Hans Boehm8a4f81c2015-07-09 10:41:25 -0700207 final CharSequence oldText = getText();
Hans Boehm24c91ed2016-06-30 18:53:44 -0700208 final char separator = KeyMaps.translateResult(",").charAt(0);
209 final CharSequence added = StringUtils.getExtensionIgnoring(newText, oldText, separator);
210 if (added != null) {
211 if (added.length() == 1) {
Hans Boehm8a4f81c2015-07-09 10:41:25 -0700212 // The algorithm for pronouncing a single character doesn't seem
213 // to respect our hints. Don't give it the choice.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700214 final char c = added.charAt(0);
Hans Boehm8a4f81c2015-07-09 10:41:25 -0700215 final int id = KeyMaps.keyForChar(c);
216 final String descr = KeyMaps.toDescriptiveString(getContext(), id);
217 if (descr != null) {
218 announceForAccessibility(descr);
219 } else {
220 announceForAccessibility(String.valueOf(c));
221 }
Hans Boehm24c91ed2016-06-30 18:53:44 -0700222 } else if (added.length() != 0) {
223 announceForAccessibility(added);
Hans Boehmccc55662015-07-07 14:16:59 -0700224 }
225 } else {
226 announceForAccessibility(newText);
227 }
Justin Klaassend1831412016-07-19 21:59:10 -0700228 setText(newText, BufferType.SPANNABLE);
Hans Boehmccc55662015-07-07 14:16:59 -0700229 }
230
Chenjie Yu3937b652016-06-01 23:14:26 -0700231 public boolean stopActionModeOrContextMenu() {
Hans Boehm1176f232015-05-11 16:26:03 -0700232 if (mActionMode != null) {
233 mActionMode.finish();
234 return true;
235 }
Chenjie Yu3937b652016-06-01 23:14:26 -0700236 if (mContextMenu != null) {
237 mContextMenu.close();
238 return true;
239 }
Hans Boehm1176f232015-05-11 16:26:03 -0700240 return false;
241 }
242
Justin Klaassenfc5ac822015-06-18 13:15:17 -0700243 public void setOnTextSizeChangeListener(OnTextSizeChangeListener listener) {
244 mOnTextSizeChangeListener = listener;
245 }
246
Christine Franks1d99be12016-11-14 14:00:36 -0800247 public void setOnContextMenuClickListener(OnFormulaContextMenuClickListener listener) {
248 mOnContextMenuClickListener = listener;
249 }
250
251 public void setOnDisplayMemoryOperationsListener(
252 Calculator.OnDisplayMemoryOperationsListener listener) {
253 mOnDisplayMemoryOperationsListener = listener;
Justin Klaassenfc5ac822015-06-18 13:15:17 -0700254 }
255
Chenjie Yu3937b652016-06-01 23:14:26 -0700256 /**
257 * Use ActionMode for paste support on M and higher.
258 */
259 @TargetApi(Build.VERSION_CODES.M)
260 private void setupActionMode() {
261 mPasteActionModeCallback = new ActionMode.Callback2() {
262
263 @Override
264 public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
265 if (onMenuItemClick(item)) {
266 mode.finish();
267 return true;
268 } else {
269 return false;
270 }
271 }
272
273 @Override
274 public boolean onCreateActionMode(ActionMode mode, Menu menu) {
275 mode.setTag(TAG_ACTION_MODE);
276 final MenuInflater inflater = mode.getMenuInflater();
Christine Franks1d99be12016-11-14 14:00:36 -0800277 return createContextMenu(inflater, menu);
Chenjie Yu3937b652016-06-01 23:14:26 -0700278 }
279
280 @Override
281 public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
282 return false;
283 }
284
285 @Override
286 public void onDestroyActionMode(ActionMode mode) {
287 mActionMode = null;
288 }
289
290 @Override
291 public void onGetContentRect(ActionMode mode, View view, Rect outRect) {
292 super.onGetContentRect(mode, view, outRect);
293 outRect.top += getTotalPaddingTop();
294 outRect.right -= getTotalPaddingRight();
295 outRect.bottom -= getTotalPaddingBottom();
Christine Franksbe739a12016-10-20 12:45:55 -0700296 // Encourage menu positioning over the rightmost 10% of the screen.
297 outRect.left = (int) (outRect.right * 0.9f);
Chenjie Yu3937b652016-06-01 23:14:26 -0700298 }
299 };
300 setOnLongClickListener(new View.OnLongClickListener() {
301 @Override
302 public boolean onLongClick(View v) {
303 mActionMode = startActionMode(mPasteActionModeCallback, ActionMode.TYPE_FLOATING);
304 return true;
305 }
306 });
307 }
308
309 /**
310 * Use ContextMenu for paste support on L and lower.
311 */
312 private void setupContextMenu() {
313 setOnCreateContextMenuListener(new OnCreateContextMenuListener() {
314 @Override
315 public void onCreateContextMenu(ContextMenu contextMenu, View view,
316 ContextMenu.ContextMenuInfo contextMenuInfo) {
317 final MenuInflater inflater = new MenuInflater(getContext());
Christine Franks1d99be12016-11-14 14:00:36 -0800318 createContextMenu(inflater, contextMenu);
Chenjie Yu3937b652016-06-01 23:14:26 -0700319 mContextMenu = contextMenu;
320 for(int i = 0; i < contextMenu.size(); i++) {
Christine Franks7452d3a2016-10-27 13:41:18 -0700321 contextMenu.getItem(i).setOnMenuItemClickListener(CalculatorFormula.this);
Chenjie Yu3937b652016-06-01 23:14:26 -0700322 }
323 }
324 });
325 setOnLongClickListener(new View.OnLongClickListener() {
326 @Override
327 public boolean onLongClick(View v) {
328 return showContextMenu();
329 }
330 });
331 }
332
Christine Franks1d99be12016-11-14 14:00:36 -0800333 private boolean createContextMenu(MenuInflater inflater, Menu menu) {
Chenjie Yu3937b652016-06-01 23:14:26 -0700334 final ClipboardManager clipboard = (ClipboardManager) getContext()
335 .getSystemService(Context.CLIPBOARD_SERVICE);
Christine Franks1d99be12016-11-14 14:00:36 -0800336 final boolean isPasteEnabled = clipboard.hasPrimaryClip();
337 final boolean isMemoryEnabled = isMemoryEnabled();
338 if (!isPasteEnabled && !isMemoryEnabled) {
339 return false;
Chenjie Yu3937b652016-06-01 23:14:26 -0700340 }
Christine Franks1d99be12016-11-14 14:00:36 -0800341
342 bringPointIntoView(length());
343 inflater.inflate(R.menu.menu_formula, menu);
344 final MenuItem pasteItem = menu.findItem(R.id.menu_paste);
345 final MenuItem memoryRecallItem = menu.findItem(R.id.memory_recall);
346 pasteItem.setEnabled(isPasteEnabled);
347 memoryRecallItem.setEnabled(isMemoryEnabled);
348 return true;
Chenjie Yu3937b652016-06-01 23:14:26 -0700349 }
350
Justin Klaassenfc5ac822015-06-18 13:15:17 -0700351 private void paste() {
352 final ClipboardManager clipboard = (ClipboardManager) getContext()
353 .getSystemService(Context.CLIPBOARD_SERVICE);
354 final ClipData primaryClip = clipboard.getPrimaryClip();
Christine Franks1d99be12016-11-14 14:00:36 -0800355 if (primaryClip != null && mOnContextMenuClickListener != null) {
356 mOnContextMenuClickListener.onPaste(primaryClip);
Justin Klaassenfc5ac822015-06-18 13:15:17 -0700357 }
358 }
359
Chenjie Yu3937b652016-06-01 23:14:26 -0700360 @Override
361 public boolean onMenuItemClick(MenuItem item) {
Christine Franks1d99be12016-11-14 14:00:36 -0800362 switch (item.getItemId()) {
363 case R.id.memory_recall:
364 mOnContextMenuClickListener.onMemoryRecall();
365 return true;
366 case R.id.menu_paste:
367 paste();
368 return true;
369 default:
370 return false;
Chenjie Yu3937b652016-06-01 23:14:26 -0700371 }
Chenjie Yu3937b652016-06-01 23:14:26 -0700372 }
373
Christine Franksafe28bb2016-07-29 17:24:52 -0700374 @Override
375 public void onPrimaryClipChanged() {
376 final ClipData clip = mClipboardManager.getPrimaryClip();
377 if (clip == null || clip.getItemCount() == 0) {
Christine Franks1d99be12016-11-14 14:00:36 -0800378 setLongClickable(isMemoryEnabled());
Christine Franksafe28bb2016-07-29 17:24:52 -0700379 return;
380 }
Annie Chin7d50adf2016-10-27 15:55:38 -0700381 CharSequence clipText = null;
382 try {
383 clipText = clip.getItemAt(0).coerceToText(getContext());
384 } catch (Exception e) {
385 Log.i("Calculator", "Error reading clipboard:", e);
386 }
Christine Franks1d99be12016-11-14 14:00:36 -0800387 setLongClickable(!TextUtils.isEmpty(clipText) || isMemoryEnabled());
388 }
389
390 private boolean isMemoryEnabled() {
391 return !(mOnDisplayMemoryOperationsListener == null || mOnContextMenuClickListener == null)
392 && mOnDisplayMemoryOperationsListener.shouldDisplayMemory();
Christine Franksafe28bb2016-07-29 17:24:52 -0700393 }
394
Justin Klaassenfed941a2014-06-09 18:42:40 +0100395 public interface OnTextSizeChangeListener {
396 void onTextSizeChanged(TextView textView, float oldSize);
397 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700398
Christine Franks1d99be12016-11-14 14:00:36 -0800399 public interface OnFormulaContextMenuClickListener {
Justin Klaassenfc5ac822015-06-18 13:15:17 -0700400 boolean onPaste(ClipData clip);
Christine Franks1d99be12016-11-14 14:00:36 -0800401 void onMemoryRecall();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700402 }
Dmitri Plotnikovde3eec22011-01-17 18:23:37 -0800403}