blob: 88f677c40906ef997660f1661384253de22e32cb [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;
Hongwei Wang245925e2014-05-11 14:38:47 -070030import android.util.TypedValue;
Gilles Debunnef57b8b42011-01-27 10:54:07 -080031import android.view.ActionMode;
Chenjie Yu3937b652016-06-01 23:14:26 -070032import android.view.ContextMenu;
Gilles Debunnef57b8b42011-01-27 10:54:07 -080033import android.view.Menu;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070034import android.view.MenuInflater;
Gilles Debunnef57b8b42011-01-27 10:54:07 -080035import android.view.MenuItem;
Hans Boehm76b78152015-04-17 10:50:35 -070036import android.view.View;
Justin Klaassenfed941a2014-06-09 18:42:40 +010037import android.widget.TextView;
Dmitri Plotnikovde3eec22011-01-17 18:23:37 -080038
Hans Boehm84614952014-11-25 18:46:17 -080039/**
Christine Franks7452d3a2016-10-27 13:41:18 -070040 * TextView adapted for displaying the formula and allowing pasting.
Hans Boehm84614952014-11-25 18:46:17 -080041 */
Christine Franks7452d3a2016-10-27 13:41:18 -070042public class CalculatorFormula extends AlignedTextView implements MenuItem.OnMenuItemClickListener,
Christine Franksafe28bb2016-07-29 17:24:52 -070043 ClipboardManager.OnPrimaryClipChangedListener {
Alan Viverette461992d2014-03-07 13:29:56 -080044
Annie Chine918fd22016-03-09 11:07:54 -080045 public static final String TAG_ACTION_MODE = "ACTION_MODE";
46
Justin Klaassenfc5ac822015-06-18 13:15:17 -070047 // Temporary paint for use in layout methods.
48 private final TextPaint mTempPaint = new TextPaint();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070049
Justin Klaassen4b3af052014-05-27 17:53:10 -070050 private final float mMaximumTextSize;
51 private final float mMinimumTextSize;
52 private final float mStepTextSize;
53
Christine Franksafe28bb2016-07-29 17:24:52 -070054 private final ClipboardManager mClipboardManager;
55
Justin Klaassen4b3af052014-05-27 17:53:10 -070056 private int mWidthConstraint = -1;
Justin Klaassenfc5ac822015-06-18 13:15:17 -070057 private ActionMode mActionMode;
Chenjie Yu3937b652016-06-01 23:14:26 -070058 private ActionMode.Callback mPasteActionModeCallback;
59 private ContextMenu mContextMenu;
Justin Klaassenfc5ac822015-06-18 13:15:17 -070060 private OnPasteListener mOnPasteListener;
Justin Klaassenfed941a2014-06-09 18:42:40 +010061 private OnTextSizeChangeListener mOnTextSizeChangeListener;
Justin Klaassen4b3af052014-05-27 17:53:10 -070062
Christine Franks7452d3a2016-10-27 13:41:18 -070063 public CalculatorFormula(Context context) {
Justin Klaassenfc5ac822015-06-18 13:15:17 -070064 this(context, null /* attrs */);
Justin Klaassen4b3af052014-05-27 17:53:10 -070065 }
66
Christine Franks7452d3a2016-10-27 13:41:18 -070067 public CalculatorFormula(Context context, AttributeSet attrs) {
Justin Klaassenfc5ac822015-06-18 13:15:17 -070068 this(context, attrs, 0 /* defStyleAttr */);
Justin Klaassen4b3af052014-05-27 17:53:10 -070069 }
70
Christine Franks7452d3a2016-10-27 13:41:18 -070071 public CalculatorFormula(Context context, AttributeSet attrs, int defStyleAttr) {
Justin Klaassenfc5ac822015-06-18 13:15:17 -070072 super(context, attrs, defStyleAttr);
Justin Klaassen4b3af052014-05-27 17:53:10 -070073
Christine Franksafe28bb2016-07-29 17:24:52 -070074 mClipboardManager = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
75
Justin Klaassen4b3af052014-05-27 17:53:10 -070076 final TypedArray a = context.obtainStyledAttributes(
Christine Franks7452d3a2016-10-27 13:41:18 -070077 attrs, R.styleable.CalculatorFormula, defStyleAttr, 0);
Justin Klaassen4b3af052014-05-27 17:53:10 -070078 mMaximumTextSize = a.getDimension(
Christine Franks7452d3a2016-10-27 13:41:18 -070079 R.styleable.CalculatorFormula_maxTextSize, getTextSize());
Justin Klaassen4b3af052014-05-27 17:53:10 -070080 mMinimumTextSize = a.getDimension(
Christine Franks7452d3a2016-10-27 13:41:18 -070081 R.styleable.CalculatorFormula_minTextSize, getTextSize());
82 mStepTextSize = a.getDimension(R.styleable.CalculatorFormula_stepTextSize,
Justin Klaassen4b3af052014-05-27 17:53:10 -070083 (mMaximumTextSize - mMinimumTextSize) / 3);
Justin Klaassen4b3af052014-05-27 17:53:10 -070084 a.recycle();
85
Chenjie Yu3937b652016-06-01 23:14:26 -070086 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
87 setupActionMode();
88 } else {
89 setupContextMenu();
90 }
Hans Boehm76b78152015-04-17 10:50:35 -070091 }
Justin Klaassen4b3af052014-05-27 17:53:10 -070092
93 @Override
94 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Justin Klaassen0ace4eb2016-02-05 11:38:12 -080095 if (!isLaidOut()) {
96 // Prevent shrinking/resizing with our variable textSize.
97 setTextSizeInternal(TypedValue.COMPLEX_UNIT_PX, mMaximumTextSize,
98 false /* notifyListener */);
Justin Klaassenf3076c82016-06-03 13:18:55 -070099 setMinimumHeight(getLineHeight() + getCompoundPaddingBottom()
Justin Klaassen0ace4eb2016-02-05 11:38:12 -0800100 + getCompoundPaddingTop());
101 }
102
Justin Klaassenf3076c82016-06-03 13:18:55 -0700103 // Ensure we are at least as big as our parent.
104 final int width = MeasureSpec.getSize(widthMeasureSpec);
105 if (getMinimumWidth() != width) {
106 setMinimumWidth(width);
107 }
108
Justin Klaassenfc5ac822015-06-18 13:15:17 -0700109 // Re-calculate our textSize based on new width.
Justin Klaassen0ace4eb2016-02-05 11:38:12 -0800110 mWidthConstraint = MeasureSpec.getSize(widthMeasureSpec)
Justin Klaassen44595162015-05-28 17:55:20 -0700111 - getPaddingLeft() - getPaddingRight();
Justin Klaassen0ace4eb2016-02-05 11:38:12 -0800112 final float textSize = getVariableTextSize(getText());
113 if (getTextSize() != textSize) {
114 setTextSizeInternal(TypedValue.COMPLEX_UNIT_PX, textSize, false /* notifyListener */);
Justin Klaassenfc5ac822015-06-18 13:15:17 -0700115 }
116
117 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700118 }
119
120 @Override
Christine Franksafe28bb2016-07-29 17:24:52 -0700121 protected void onAttachedToWindow() {
122 super.onAttachedToWindow();
123
124 mClipboardManager.addPrimaryClipChangedListener(this);
125 onPrimaryClipChanged();
126 }
127
128 @Override
129 protected void onDetachedFromWindow() {
130 super.onDetachedFromWindow();
131
132 mClipboardManager.removePrimaryClipChangedListener(this);
133 }
134
135 @Override
Justin Klaassen4b3af052014-05-27 17:53:10 -0700136 protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
137 super.onTextChanged(text, start, lengthBefore, lengthAfter);
Justin Klaassenbfc4e4d2014-08-27 10:56:49 -0700138
Justin Klaassen4b3af052014-05-27 17:53:10 -0700139 setTextSize(TypedValue.COMPLEX_UNIT_PX, getVariableTextSize(text.toString()));
140 }
141
Hans Boehm11e37a82015-10-01 14:41:37 -0700142 private void setTextSizeInternal(int unit, float size, boolean notifyListener) {
Justin Klaassenfed941a2014-06-09 18:42:40 +0100143 final float oldTextSize = getTextSize();
144 super.setTextSize(unit, size);
Hans Boehm11e37a82015-10-01 14:41:37 -0700145 if (notifyListener && mOnTextSizeChangeListener != null && getTextSize() != oldTextSize) {
Justin Klaassenfed941a2014-06-09 18:42:40 +0100146 mOnTextSizeChangeListener.onTextSizeChanged(this, oldTextSize);
147 }
148 }
149
Hans Boehm11e37a82015-10-01 14:41:37 -0700150 @Override
151 public void setTextSize(int unit, float size) {
152 setTextSizeInternal(unit, size, true);
153 }
154
Justin Klaassen44595162015-05-28 17:55:20 -0700155 public float getMinimumTextSize() {
156 return mMinimumTextSize;
157 }
158
159 public float getMaximumTextSize() {
160 return mMaximumTextSize;
161 }
162
Justin Klaassenfc5ac822015-06-18 13:15:17 -0700163 public float getVariableTextSize(CharSequence text) {
Justin Klaassen4b3af052014-05-27 17:53:10 -0700164 if (mWidthConstraint < 0 || mMaximumTextSize <= mMinimumTextSize) {
165 // Not measured, bail early.
166 return getTextSize();
167 }
168
Justin Klaassen2be4fdb2014-08-06 19:54:09 -0700169 // Capture current paint state.
170 mTempPaint.set(getPaint());
171
172 // Step through increasing text sizes until the text would no longer fit.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700173 float lastFitTextSize = mMinimumTextSize;
174 while (lastFitTextSize < mMaximumTextSize) {
Justin Klaassenfc5ac822015-06-18 13:15:17 -0700175 mTempPaint.setTextSize(Math.min(lastFitTextSize + mStepTextSize, mMaximumTextSize));
176 if (Layout.getDesiredWidth(text, mTempPaint) > mWidthConstraint) {
Justin Klaassen4b3af052014-05-27 17:53:10 -0700177 break;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700178 }
Justin Klaassenfc5ac822015-06-18 13:15:17 -0700179 lastFitTextSize = mTempPaint.getTextSize();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700180 }
181
182 return lastFitTextSize;
183 }
184
Hans Boehmccc55662015-07-07 14:16:59 -0700185 private static boolean startsWith(CharSequence whole, CharSequence prefix) {
186 int wholeLen = whole.length();
187 int prefixLen = prefix.length();
188 if (prefixLen > wholeLen) {
189 return false;
190 }
191 for (int i = 0; i < prefixLen; ++i) {
192 if (prefix.charAt(i) != whole.charAt(i)) {
193 return false;
194 }
195 }
196 return true;
197 }
198
199 /**
200 * Functionally equivalent to setText(), but explicitly announce changes.
201 * If the new text is an extension of the old one, announce the addition.
202 * Otherwise, e.g. after deletion, announce the entire new text.
203 */
204 public void changeTextTo(CharSequence newText) {
Hans Boehm8a4f81c2015-07-09 10:41:25 -0700205 final CharSequence oldText = getText();
Hans Boehm24c91ed2016-06-30 18:53:44 -0700206 final char separator = KeyMaps.translateResult(",").charAt(0);
207 final CharSequence added = StringUtils.getExtensionIgnoring(newText, oldText, separator);
208 if (added != null) {
209 if (added.length() == 1) {
Hans Boehm8a4f81c2015-07-09 10:41:25 -0700210 // The algorithm for pronouncing a single character doesn't seem
211 // to respect our hints. Don't give it the choice.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700212 final char c = added.charAt(0);
Hans Boehm8a4f81c2015-07-09 10:41:25 -0700213 final int id = KeyMaps.keyForChar(c);
214 final String descr = KeyMaps.toDescriptiveString(getContext(), id);
215 if (descr != null) {
216 announceForAccessibility(descr);
217 } else {
218 announceForAccessibility(String.valueOf(c));
219 }
Hans Boehm24c91ed2016-06-30 18:53:44 -0700220 } else if (added.length() != 0) {
221 announceForAccessibility(added);
Hans Boehmccc55662015-07-07 14:16:59 -0700222 }
223 } else {
224 announceForAccessibility(newText);
225 }
Justin Klaassend1831412016-07-19 21:59:10 -0700226 setText(newText, BufferType.SPANNABLE);
Hans Boehmccc55662015-07-07 14:16:59 -0700227 }
228
Chenjie Yu3937b652016-06-01 23:14:26 -0700229 public boolean stopActionModeOrContextMenu() {
Hans Boehm1176f232015-05-11 16:26:03 -0700230 if (mActionMode != null) {
231 mActionMode.finish();
232 return true;
233 }
Chenjie Yu3937b652016-06-01 23:14:26 -0700234 if (mContextMenu != null) {
235 mContextMenu.close();
236 return true;
237 }
Hans Boehm1176f232015-05-11 16:26:03 -0700238 return false;
239 }
240
Justin Klaassenfc5ac822015-06-18 13:15:17 -0700241 public void setOnTextSizeChangeListener(OnTextSizeChangeListener listener) {
242 mOnTextSizeChangeListener = listener;
243 }
244
245 public void setOnPasteListener(OnPasteListener listener) {
246 mOnPasteListener = listener;
247 }
248
Chenjie Yu3937b652016-06-01 23:14:26 -0700249 /**
250 * Use ActionMode for paste support on M and higher.
251 */
252 @TargetApi(Build.VERSION_CODES.M)
253 private void setupActionMode() {
254 mPasteActionModeCallback = new ActionMode.Callback2() {
255
256 @Override
257 public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
258 if (onMenuItemClick(item)) {
259 mode.finish();
260 return true;
261 } else {
262 return false;
263 }
264 }
265
266 @Override
267 public boolean onCreateActionMode(ActionMode mode, Menu menu) {
268 mode.setTag(TAG_ACTION_MODE);
269 final MenuInflater inflater = mode.getMenuInflater();
270 return createPasteMenu(inflater, menu);
271 }
272
273 @Override
274 public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
275 return false;
276 }
277
278 @Override
279 public void onDestroyActionMode(ActionMode mode) {
280 mActionMode = null;
281 }
282
283 @Override
284 public void onGetContentRect(ActionMode mode, View view, Rect outRect) {
285 super.onGetContentRect(mode, view, outRect);
286 outRect.top += getTotalPaddingTop();
287 outRect.right -= getTotalPaddingRight();
288 outRect.bottom -= getTotalPaddingBottom();
Christine Franksbe739a12016-10-20 12:45:55 -0700289 // Encourage menu positioning over the rightmost 10% of the screen.
290 outRect.left = (int) (outRect.right * 0.9f);
Chenjie Yu3937b652016-06-01 23:14:26 -0700291 }
292 };
293 setOnLongClickListener(new View.OnLongClickListener() {
294 @Override
295 public boolean onLongClick(View v) {
296 mActionMode = startActionMode(mPasteActionModeCallback, ActionMode.TYPE_FLOATING);
297 return true;
298 }
299 });
300 }
301
302 /**
303 * Use ContextMenu for paste support on L and lower.
304 */
305 private void setupContextMenu() {
306 setOnCreateContextMenuListener(new OnCreateContextMenuListener() {
307 @Override
308 public void onCreateContextMenu(ContextMenu contextMenu, View view,
309 ContextMenu.ContextMenuInfo contextMenuInfo) {
310 final MenuInflater inflater = new MenuInflater(getContext());
311 createPasteMenu(inflater, contextMenu);
312 mContextMenu = contextMenu;
313 for(int i = 0; i < contextMenu.size(); i++) {
Christine Franks7452d3a2016-10-27 13:41:18 -0700314 contextMenu.getItem(i).setOnMenuItemClickListener(CalculatorFormula.this);
Chenjie Yu3937b652016-06-01 23:14:26 -0700315 }
316 }
317 });
318 setOnLongClickListener(new View.OnLongClickListener() {
319 @Override
320 public boolean onLongClick(View v) {
321 return showContextMenu();
322 }
323 });
324 }
325
326 private boolean createPasteMenu(MenuInflater inflater, Menu menu) {
327 final ClipboardManager clipboard = (ClipboardManager) getContext()
328 .getSystemService(Context.CLIPBOARD_SERVICE);
329 if (clipboard.hasPrimaryClip()) {
330 bringPointIntoView(length());
331 inflater.inflate(R.menu.paste, menu);
332 return true;
333 }
334 // Prevents the selection action mode on double tap.
335 return false;
336 }
337
Justin Klaassenfc5ac822015-06-18 13:15:17 -0700338 private void paste() {
339 final ClipboardManager clipboard = (ClipboardManager) getContext()
340 .getSystemService(Context.CLIPBOARD_SERVICE);
341 final ClipData primaryClip = clipboard.getPrimaryClip();
342 if (primaryClip != null && mOnPasteListener != null) {
343 mOnPasteListener.onPaste(primaryClip);
344 }
345 }
346
Chenjie Yu3937b652016-06-01 23:14:26 -0700347 @Override
348 public boolean onMenuItemClick(MenuItem item) {
349 if (item.getItemId() == R.id.menu_paste) {
350 paste();
351 return true;
352 }
353 return false;
354 }
355
Christine Franksafe28bb2016-07-29 17:24:52 -0700356 @Override
357 public void onPrimaryClipChanged() {
358 final ClipData clip = mClipboardManager.getPrimaryClip();
359 if (clip == null || clip.getItemCount() == 0) {
360 setLongClickable(false);
361 return;
362 }
363 final CharSequence clipText = clip.getItemAt(0).coerceToText(getContext());
364 setLongClickable(!TextUtils.isEmpty(clipText));
365 }
366
Justin Klaassenfed941a2014-06-09 18:42:40 +0100367 public interface OnTextSizeChangeListener {
368 void onTextSizeChanged(TextView textView, float oldSize);
369 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700370
Justin Klaassenfc5ac822015-06-18 13:15:17 -0700371 public interface OnPasteListener {
372 boolean onPaste(ClipData clip);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700373 }
Dmitri Plotnikovde3eec22011-01-17 18:23:37 -0800374}